NOTE: This programming guide introduces the HOOPS Visualize API. For a high-level overview of what HOOPS Visualize is, as well as a discussion of its major components, see the Technical Overview.
The foundation of HOOPS Visualize is the Visualize graphics database. The database is where all scene information is stored and organized.
Your application will probably integrate many different libraries to provide all of its functionality. To your application, Visualize simply appears as another library containing functions which can be called to insert and delete objects from the graphics database, to initiate and control rendering, and to receive input events. The general order in which these operations occur is described below:
- Your application code makes changes to the graphics database.
- When it is finished making changes to the graphics database, your application tells Visualize to update the display (either by explicitly calling Update() or by waiting for input).
- Visualize walks the database and sends the primitives to be drawn to the renderer.
- The renderer draws the scene and sends it to some output device.
- The user sees the new picture, thinks for a while, and then generates input through the user interface.
- Your application code receives this input and makes further changes to the Visualize database.
Although called a 'database', the Visualize database does not have a relational model like a SQL database. Instead, information is organized in a tree-like scene graph composed of nodes. Nodes have a parent/child relationship where one parent can have many children but each child only has one parent. In the Visualize database, a node is called a segment. Segments are the building blocks of the scene graph - they contain all geometry and attributes and are used to organize your scene hierarchically.
Describing a scene graph as a data structure is beyond the scope of this document, but understanding how the HOOPS Visualize database works is critical in order to work with Visualize efficiently.
The database and all objects within it depend on a HPS::World object. You can think of HPS::World as the context within which Visualize is able to manage your scene and to execute all its internal commands. You must create a HPS::World before using HOOPS Visualize.
The HPS::World object requires one parameter, your license key. The Tech Soft 3D Developer Zone provides this in the form of a header file, hoops_license.h. The license key is inside that file.
C++ users may forgo setting the license key in the HPS::World constructor and instead simply use the macro HOOPS_LICENSE, which is defined in hoops_license.h. In order to make this work, you have to generate your own hoops_license.h from the Tech Soft 3D Developer Zone and copy it to <VISUALIZE_INSTALL_DIR>/include.
There are a number of different ways to connect HOOPS Visualize to a window. The simplest way uses a stand-alone window:
Despite its brevity, the code in the snippet above is all that is needed to get HOOPS Visualize running in an empty window. For further information on the other ways to connect HOOPS Visualize to your application window, see this section.
When you instantiate a HPS::World object, Visualize initializes the graphical database. To begin building your scene graph, you must utilize at least one root segment. There are two ways to do this. You may use a window segment as a root segment, or you can create one independently using HPS::Database::CreateRootSegment and then associate it with a window segment using an include link. Any number of root segments can be created this way. This method is useful, for example, when you have multiple geometries or assemblies you would like to swap in and out of the scene. Note that the geometry in a root segment is not rendered unless it has a window segment as an ancestor (see this subsection for details on attaching segments).
Regardless of which method you choose, it is important to understand that only those segments that have a window segment ancestor will be drawn at render-time. With a root segment, you can set attributes, insert geometry and create subsegments. A window segment is similar to a root segment but has slightly different behavior and attributes. For example, every window segment is associated with a Visualize rendering context while a plain root segment is not (a plain root segment may not even be attached to the scene hierarchy). Another difference is that window segments are the only place to set window-level rendering directives such as anti-aliasing. It is important to understand these differences to determine which one to use and when to use it.
If a root segment does not need to be attached to the main scene graph, then what is its purpose? A root segment may be used to organize a group of related segments into a model subassembly to which attributes such as visibility or modelling matrix may be applied. For example, to temporarily make a subassembly invisible, you could detach the corresponding branch from the scene tree. The root of the branch is now a root segment. Another example would be to create a new root segment using Database::CreateRootSegment, fill its branches with model data, and attach it to the main tree when it is ready to be rendered.
In the sample code above, both a generic root segment and a window segment are created using static methods in HPS::Database. Both creation methods return a HPS::SegmentKey or subclass thereof. Note that a HPS::SegmentKey is not actually a segment but a handle that lets you interact with a specific instance of a segment in the database. A HPS::SegmentKey provides access to the attributes and geometry within a segment and allows you to perform actions upon the segment. It also lets you traverse the segment tree allowing you to access and manipulate child segments.
In summary, all segments are inherently part of the database - they do not exist in isolation. All segments have a parent except root segments which exhibit slightly different behavioral relationships. Each segment has a handle called a key. The key allows you to locate the segment, find its type, and find its relationship to other segments like its parent and children. The following code snippet demonstrates some examples of these interactions:
A segment contains data and also has data attached to it. The way you structure your scene graph is very important, because a segment always inherits a set of attributes from its parent segment. Isolated root segments do not contain initialized attributes, but they will inherit attributes from a parent when attached. In Visualize, attaching is called inclusion. In addition to inserting geometry into a segment and creating child segments, it is possible to attach or detach existing segments. However, since normal segments cannot exist in an orphaned state, when detaching segments, they must be either deleted or moved to another segment (this restriction does not apply to root segments or include segments).
One of the main purposes of segments is to group attributes at the segment level. For performance reasons, segments are intended to be used to group items with similar attributes. Attributes are descriptors that tell Visualize how to render graphical information. Most (but not all) attributes exist at the segment level, which means they apply to all geometry in the segment. For any attribute set, there exists exactly one value for each attribute. If you do not explicitly specify the value for an attribute on a segment, the effective value will be inherited from the segment's parent (or ancestor, if the parent has no setting of its own). This also means that assigning a value to an attribute will cause all subsegments to inherit that value, unless they override it with a value of their own. Visualize determines a set of default values for all attributes and initializes each window segment with those defaults. Thus, due to the nature of inheritance, your attribute will receive the default value if you do not explicitly set a value for it in an ancestor segment.
A further discussion on attributes, as well as examples on setting attributes at the segment level, can be found in the attributes section. Default values for all attributes can be found in the appendix.
When a segment is created, it contains no geometry. You can insert any type of geometry into a segment. When geometry is inserted into a segment, it becomes part of the database. Each insert function returns a key which is used as the geometry handle (see that snippet for a basic example of geometry insertion). Keys are reference counted smart pointers that do not delete the database object when they go out of scope. The key helps locate the geometry in the database letting us know its relationships to parent and children. Geometry is always part of a segment, but not all segments must contain geometry.
Segments can contain other segments, thus, child segments can have their own subsegments. This data model is designed to allow you to build a segment tree that logically describes your graphical scene. Common use of Visualize involves creating a segment hierarchy by attaching segments to other segments (see section 7.3.1 for information about recommended segment organization). To create a subsegment:
When calling Down or Subsegment, you may supply a name for the segment. Any UTF-8 encoded name is legal. You can pass an empty string for an anonymous name, and in this case Visualize will guarantee a unique segment.
Graphical information inserted into a window segment ultimately goes into the rendering pipeline. However, sometimes it may be desirable to create and manipulate data without drawing immediately. For instance, imagine creating a scene graph for a turbine engine. Such a model would likely be very complex. You know that you will want to display your turbine engine in multiple windows at different times. Additionally, you might also want to export the information to a PDF file or print it to hardcopy. However, duplicating a scene graph is tedious, wastes memory, and is prone to errors. Additionally, if modifications are made to the scene, it must be made in multiple locations in order to maintain synchronicity with all scene graphs.
A more efficient way to manage the model data would be to build one scene graph which can be referenced by multiple segments. To begin constructing your scene graph, request a root segment from the Visualize database. With this root segment, you can build a segment tree that represents your model without associating it with any particular window.
When you request a root segment, Visualize creates a parentless segment and returns a HPS::SegmentKey. You can then use the key to build your model's information hierarchy - setting attributes and inserting geometry. An isolated root segment and the information associated is never drawn unless it has a Visualize window as an ancestor. Therefore, to display your model, you must include your subtree into a window segment.
Once a segment is included by a window key, it will enter the rendering pipeline for that window. Multiple segments can include the model, thus, copies of the geometry can be drawn at multiple locations in a scene or in multiple windows. A change in the model will be reflected in all instances. You can also export your model information to a file or print to hardcopy.
This paradigm for creating and storing model information can also be used to organize and aggregate data. For instance, you can create multiple model segments and then use them to construct a master assembly that is itself a root segment. As an example, let's take the turbine engine. You may want to display the turbine engine by itself. However, you may also want to display it as part of the design for a whole aircraft. In each aircraft's scene graph, you can include the turbine engine using its HPS::SegmentKey. Any changes in the turbine engine will be reflected in all models that include the turbine engine.
Judicious use of model segments in conjunction with window segments will help you efficiently manage and render your graphical data with minimal effort.
Filtering include links
When an include link is employed, all geometry and attributes from the target segment are included by the parent segment. It is possible to set a filter on the include key so that certain geometry or attributes can be excluded.
Deleting include links
To sever the link between the parent key and its included segment, you must delete the include link key:
Conditional include links
Sometimes, it is useful to include a branch of a tree only when certain conditions are met. The conditional include mechanic provides this functionality. A condition is really just a flag that is set on a segment. When Visualize detects that the flag is set, the included segment is made part of the main tree and thus its geometry is rendered.
While the code above is a trivial example of conditional include links, the real power of conditional includes becomes more apparent when many links need to be made at multiple locations in the tree. Conditions are inheritable attributes, and thus if set at the root segment, many links can be automatically included in one simple command if they all depend on the same condition.
In some situations, you may find it necessary to instantiate geometric information at multiple places in a scene without having to worry about how the geometry is styled. Segment references enable you to do this. However, unlike include segments, segment references only import geometry - attributes and subsegments are excluded. A segment may be referenced any number of times.
In addition to segments, all other database objects in Visualize have an associated key. For example, a HPS::TextKey identifies a text string, and a HPS::SphereKey identifies a sphere. All Database modifications involve keys at some level. For example, to modify the font of a text string, you would use its key. In the code below, a simple segment is constructed. Note the relationships between the keys of the root segment, the sphere, the text, and the text font:
Behind the scenes, all Visualize keys are reference-counted smart pointers. You don't have to worry about initializing or deleting them. When a key goes out of scope, the object still exists in the Database.
You can use an object's key to set its fields. For example, you may use a HPS::CircleKey to set the radius and center point of a circle:
Note that setting attributes at the geometry level as shown in the snippet above will affect only the geometry represented by myCircleKey. However, most attributes in Visualize are set at the segment level and use attribute inheritance to propagate values to geometry in child segments. An easy way to set attributes on a segment is to use an attribute control, as discussed in the following section.
An attribute is a setting that is used to style or alter how a segment's geometry is rendered. For each primitive, every applicable attribute must be known before the primitive can be drawn. For example, before a line can be drawn, the renderer must know the line's color, thickness, style, modelling transformation, how it is lit, and so on. A primitive can have dozens - or even hundreds - of attributes; specifying them all would be substantial work. The concept of attribute inheritance addresses this issue.
Attribute inheritance in Visualize works like inheritance in an object-oriented language. If an attribute value is not set locally on a segment, then it is inherited from a parent segment. When the database is rendered, it needs to know the attribute values for all the geometry in the database. If any required attributes have not been set on a segment, the value in the parent segment will be used. If the parent has no value, then Visualize looks at the parent's parent, and so on, until it finds a value for the attribute. The window segment of every scene graph always has a set of default attributes which will be used if no other value is set. Default values for all attributes can be found in the appendix.
Some attributes, such as color and visibility, can be set at the segment level, geometry level, or subentity level. While this gives developers the maximum amount of flexibility in building a scene, setting attributes at the segment level is recommended for optimum performance. General performance guidelines are discussed in our section on performance considerations.
IMPORTANT: Generally speaking, attribute inheritance only applies to segment-level attributes. Geometry attributes, such as the radius of a sphere, only apply to a single piece of geometry and do not inherit. If you can alter an attribute using an attribute control obtained from a HPS::SegmentKey, you know this is a segment-level attribute. For example, this snippet shows inheritable line attributes being set using a HPS::LineAttributeControl. This code sample shows non-inheritable geometry attributes being set on a circle using a HPS::CircleKey.
Attribute controls are discussed in the following subsection.
Setting and getting attributes using attribute controls
Unlike HPS::CircleKey, not every type of geometry key has its own set of attribute accessor functions. For these objects, attributes are specified using an attribute control. Attribute controls are available for all entities that are set at the segment level. When a segment-level attribute is set, all geometry within that segment and within all subsegments will inherit that setting. Of course, it is possible to override this by setting another attribute further down in the tree, or by using an attribute lock.
In this example, the edge and camera controls are demonstrated, however, the other controls follow the same pattern. All of the controls are listed with the other classes in the Class Index. For an in-depth example on setting attributes and using inheritance, see Tutorial 1.
Example: Edge attribute control
Edge attributes are controlled using the HPS::EdgeAttributeControl. The attributes that can be modified are edge weight and edge pattern. Edge patterns and line patterns can be used interchangeably (line patterns are discussed in detail in section 4.6). Continuing from the circle example, the results of the following code can be seen below:
Example: Camera attribute control
Getting attribute values
Note that you can also use attribute controls to get a segment's locally set attributes. For example, in the code snippet above, ShowProjection is used to get the camera projection. Getting attributes this way only works for attributes explicitly set on a segment. To find the value of inherited attributes, you need to use a HPS::KeyPath. That procedure is documented here.
Kits and geometry keys are related. They share the same settings. For most (but not all) types of keys, there is a corresponding kit type. Key objects always represent an object inside the Visualize database while kits exist entirely outside the database. You can think of a kit as a non-instanced key. Kits are useful because you can set all attributes for a future object in an independent way, without affecting the scene. Then, it can be inserted as a finished object into the database, which offers a modest performance benefit over building the same object piece-wise. Note that the kit and the resulting geometry are two separate objects. If you insert geometry using a kit, the geometry will take on the attributes of kit in the state it is in at the time of the insertion. If the kit is later changed, there will be no effect on the inserted object. Some other uses for kits include:
- inserting multiple identical or nearly-identical copies of an object into the database
- retrieving database information into one convenient object
- inserting a complex object into the database in a transactional way
The code sample below uses a HPS::CircleKit to insert two different circles using the same kit.
The result is show below. Note that face visibility has also been set to 'false' and a marker has been placed at the origin.
Attributes can also be set without the use of a kit. In that case, the Set functions would be called directly on the object itself. Note that this method incurs a performance penalty when compared to using a kit, as each attribute must be inserted into the database atomically instead of in a transactional way.
It is possible to temporarily override attributes by using an attribute lock. When a specific attribute is locked in a segment, that attribute will apply to all subsegments, even if they have a setting for the same attribute. Attribute locks may be useful when editing the attributes in a model. For example, you may display a model subassembly which already has its own attributes - but you want a 'parent' model object to have its attributes take precedence and override its children. Iterating through all the associated segments to change their attributes individually would be cumbersome - this problem is solved by attribute locks.
Please note that an attribute lock is itself an attribute. So, to override a lock set by an ancestor segment, set an attribute lock on a subsegment. Additionally, when setting an attribute lock on a segment which also has an applied style, the lock will be applied to the naturally inherited attributes - not the styled attributes.
IMPORTANT: Attribute lock provides a feature for temporary rendering instances. We do not recommend that you use this feature in place of setting attributes in the normal way. When this feature is enabled, rendering performance can be impacted.
Interally, HOOPS Visualize stores attribute settings in related groups. For example, all backface diffuse settings (BackFaceDiffuseColor, BackFaceDiffuseTexture, BackFaceDiffuseAlpha) are assigned to the BackFaceDiffuse attribute lock group.
As a result, when you inspect which locks are currently set, the expected result is affected by which settings are active. If all settings of a lock group are active, HOOPS Visualize will return the group setting. Let's say you set locks for BackFaceDiffuseColor and BackFaceDiffuseAlpha. If you use HPS::AttributeLockControl::ShowLock, it will return those two locks as expected.
However, when showing active locks in a situation where BackFaceDiffuseColor, BackFaceDiffuseTexture, BackFaceDiffuseAlpha are all set, only BackFaceDiffuse would be returned. This is because in this context, BackFaceDiffuse = BackFaceDiffuseColor + BackFaceDiffuseTexture + BackFaceDiffuseAlpha. The analogous situation is true for all types of material locks (FrontFaceDiffuse, CutFaceDiffuse, etc).
From a user perspective, the conventional concept of a window is a logical division of screen space controlled by one application. However, in Visualize, the term "window" is akin to a rendering context - the GUI part of the window you see on the screen is still handled by the native GUI framework. Behind the scenes, a Visualize window is represented by a segment in the scene graph. As such, each window has a unique key to identify it. Visualize offers three different types of windows, as discussed below. Regardless of the actual type of window, all window keys are derived from HPS::WindowKey.
Window segments are the top-level node of a particular scene. The window segment is also where scene-level attributes, such as anti-aliasing, bloom, depth of field (see HPS::WindowKey::SetPostProcessEffects), and other rendering algorithms are set. Some options need to be set before window creation, and for that case, each window type has its own associated window options kit.
A window is always associated with a driver interface, through which all rendering is performed. For on-screen drivers, Visualize includes implementations of DX11, OpenGL, and OpenGL2. While possible to create a multi-windowed Visualize application that uses different display drivers for each window, there can only be one driver per window. It is also not possible to change drivers after a window has been created.
While drivers can be manually specified, Visualize can also perform runtime query of the 3D capabilities of the operating system and GPU, and automatically choose the most appropriate display driver. Under Windows, preference will be given to DirectX API, followed by OpenGL. Under all other platforms, only the OpenGL API is supported. To use this capability, choose HPS::Window::Driver::Default3D when creating your window (window creation is covered below).
The application window is the most common type of window used in a Visualize application. It is represented by HPS::ApplicationWindowKey. For all but the simplest programs, this is the appropriate construct to use. For example, an application window can be hosted by a .NET UserControl, an MFC dialog, or even a plain Win32 window.
In fact, Visualize can render to any window for which you provide a handle. In Windows, the handle is the HWND parameter and is passed to the Database as shown below. Note the returned key from CreateApplicationWindow, which is used to control the scene at a high level. For example, window keys are used to set selection options and trigger updates.
Although technically a window in Visualize, the offscreen window isn't visible to the user. This type of window is a memory buffer. However, it is still a completely functional Visualize rendering context. Offscreen windows are discussed in detail in section 7.2.
The standalone window is used when no host window is provided to host Visualize. In this situation, Visualize will create a default window and use it for rendering. The standalone window may be useful for quick prototyping, testing, or for very simple applications without GUI controls. The Visualize rendering context will use the entire window for rendering. The code below is a complete program that will result in a blank Visualize window:
To set the size of a standalone window, use the HPS::StandAloneWindowOptionsKit to set the subscreen:
In Visualize, a definition is an entity that encapsulates a type of styling. Unlike segments, they do not have parents, children or siblings. Instead, they belong to a portfolio. A portfolio is a named area where definitions are created and stored. An example of a definition is a texture, line pattern, or custom glyph. Organizing these resources into a portfolio makes reuse relatively simple.
Like all other database elements, every portfolio has a unique key associated with it called a HPS::PortfolioKey. Portfolios can be shared among segments. Once the styles have been organized into different portfolios, they can be used in a scene graph. A single segment can use only one portfolio at a time, however, child segments inherit all definitions from their parents' portfolios and can augment or replace them by definitions in their own portfolio.
To learn more about portfolios and definitions, please see the portfolios and definitions discussion beginning in section 4.
Using a HPS::SegmentKey, it is possible to get the parent and children segments using the Up() and Down() methods.
Note that if Up() is called on a segment that has no parent, such as a root segment or a window key, the returned key will be invalid. If you try to use an invalid key an HPS::InvalidObjectException will be thrown. Therefore, if there is any doubt about the validity of a HPS::SegmentKey, check the Type():
Often, HPS::Database traversal is too limited or cumbersome to use when you already know what you're looking for, especially when the scene graph is extensive. In this case, a Database search is more appropriate. Searches are always done using the HPS::SegmentKey::Find function, and can be used to find objects within a segment, any subsegment, or all subsegments and included segments. The results are returned in a HPS::SearchResults object. Imagine you are using a Database search to find all circles in your scene. One way to do this would be the following:
IMPORTANT: Using the Type() function has performance implications. Please see our section on using keys for details.
IMPORTANT: Attributes and user data set on geometry is not searched using the database search. If your application depends on the search feature, you should plan to set attributes at the segment level.
An overload to HPS::SegmentKey::Find accepts a HPS::SearchOptionsKit. In addition to the criteria and search space parameters offered by the example above, behavior is also an option when using the search options kit. The search behavior can be set to either HPS::Search::Behavior::FirstMatch or HPS::Search::Behavior::Exhaustive. Exhaustive will return all matches in the subtree you specify whereas FirstMatch only returns the first item that matches your search criteria.
Mapping scene graph data to application data can be useful in a variety of situations - especially when performing a query or responding to a selection. The HOOPS Visualize database allows you to associate your external data with segments or with individual pieces of geometry. Visualize calls this data user data. User data is optional, and is always supplied and interpreted by the developer.
All user data is stored in the Database as a byte array. Any HPS::GeometryKey or HPS::SegmentKey can have user data. User data does not conform to the a traditional attribute rules in that it does not inherit to child segments.
Any number of user data entries can be present on a particular segment or geometry key. When setting the data, you choose an index on which the data is placed. Indices do not have to be sequential, but must be unique within the segment (indices that are already in use will be overwritten). The code snippet below demonstrates how to set, retrieve, and unset user data.
NOTE: When loading an HSF file, any user options present in the file will be UTF8 encoded and stored as user data.