Skip to content

Model Viewer Programming Discussion

Gary edited this page Mar 10, 2015 · 3 revisions

Table of Contents

Games contain graphic models of game objects, such as characters and vehicles. The ATF Model Viewer Sample renders 3D models of objects in ATGI and Collada format files, with .atgi and .dae extensions. The model can be rendered in variety of ways, as a wireframe, for instance.

Besides rendering model data, this sample illustrates handling documents, commands, and controls in ATF. Although this application does not have its own data model or edit data, it still makes use of the ATF DOM by treating the model data as a tree of DOM nodes.

Programming Overview

This topic only briefly touches on ATGI and Collada. While largely routine, the MEF initialization for ModelViewer uses the Component Model API to get an exported component.

Although ModelViewer does not edit data, it makes use of the ATGI and Collada data models expressed in their XML Schema. It uses the AtgiResolver and ColladaResolver components, which implement IResourceResolver to both load the schema files and to load model files, creating DomNode trees from their data.

ModelViewer treats the loaded data as a document. Its ModelDocument implements IDocument and its document client class ModelViewer implements IDocumentClient.

RenderView registers a DesignControl to display a 3D scene from the active document. The RenderCommands component works with RenderView to change the rendering mode. RenderView builds a scene graph from the model's DomNode tree. A DesignControl traverses the scene graph to render the model: each IRenderObject object in the scene graph is traversed to create a TraverseNode instance and add it to the traverse list. This results in the Traverse() method in the DOM adapters RenderTransform and RenderPrimitive being called for each IRenderObject. RenderTransform transforms model objects properly, and RenderPrimitives does the lowest level rendering using OpenGL®. OpenGL® is not discussed here, because it is distinct from ATF and well-documented elsewhere.

ATGI and Collada File Formats

ATGI and Collada offer formats for exchanging digital assets among graphics software applications. ATGI is a Sony intermediate file format for game art data. COLLADA (from collaborative design activity) defines an open standard format.

Both provide XML schema defining the formats' types. Collada model files have the extension .dae (digital asset exchange). ATGI model files have the .atgi extension.

For details on Collada, see http://collada.org.

MEF Initialization

ModelViewer creates a MEF TypeCatalog as many samples do and uses many of the common components, such as ControlHostService and StandardFileCommands. It uses several standard components to open model files:

  • AtgiResolver: resource resolver for ATGI files.
  • ColladaResolver: resolves COLLADA resource files.
ModelViewer also adds several components of its own:
  • ModelViewer: document client for 3D model files, opening model files.
  • RenderView: registers a DesignControl to display a 3D scene from a model document.
  • RenderCommands: provides user commands related to change the view of the model.
After the usual CompositionContainer, CompositionBatch, and MainForm creation and handling, the sample application determines which menu items appear under the File menu, just Open in this case. This must be set before component initialization, which immediately follows:
StandardFileCommands stdfile = container.GetExportedValue<StandardFileCommands>();
stdfile.RegisterCommands = StandardFileCommands.CommandRegister.FileOpen;

// Initialize components
foreach (IInitializable initializable in container.GetExportedValues<IInitializable>())
    initializable.Initialize();

The ExportProvider.GetExportedValue<T>() method gets the exported value for the StandardFileCommands component and sets its RegisterCommands property, which contains a mask for the File menu items that are used. This property must be set before StandardFileCommands is initialized.

After this, the sample directly initializes the components by calling Initialize() for every component in the CompositionContainer object, but it could just as well have used the IInitializable.InitializeAll() extension method like this, which is what most samples do:

container.InitializeAll();

The advantage of this latter method is that it first instantiates the components, because components are loaded in a lazy fashion otherwise, only created when required to satisfy another component's import.

Model Data Handling

ModelViewer can view two different kinds of model data: from ATGI and from Collada. Although these two data models are different, they can both be specified in the XML Schema Definition Language (XSD), also known as XML Schemas. The data model defines a set of data types that describe the data in a model file. ATF includes two type definition files in this format, "atgi.xsd" and "collada.xsd".

The ATF DOM can use XML Schemas for its data models, and many of the ATF DOM's facilities are used in the ModelViewer sample. For details, see the ATF Programmer's Guide: Document Object Model (DOM), downloadable at ATF Documentation.

Resource Resolvers

How does the ModelViewer use the XML Schema for ATGI and Collada? It imports the available IResourceResolver objects, the AtgiResolver and ColladaResolver components. These resolvers do two things:

  • Load the schema file for model data.
  • Provide a Resolve() method to load a model file from a given URI.

Loading Schema File

The Initialize() method for each resolver component loads the model schema. Here is Initialize() for AtgiResolver:

public void Initialize()
{
    if (m_initialized)
        return;

    m_initialized = true;

    Assembly assembly = Assembly.GetExecutingAssembly();
    m_loader.SchemaResolver = new ResourceStreamResolver(assembly, assembly.GetName().Name + "/schemas");
    m_loader.Load("atgi.xsd");
}

The m_loader field is initialized this way:

private AtgiSchemaTypeLoader m_loader = new AtgiSchemaTypeLoader();

AtgiSchemaTypeLoader derives from XmlSchemaTypeLoader, which can load schema files with its Load() method. AtgiSchemaTypeLoader uses this load method to load the ATGI schema file "atgi.xsd" after resolving its URI.

ColladaResolver uses the ColladaSchemaTypeLoader, also derived from XmlSchemaTypeLoader, to load the Collada schema.

Both ATGI and Collada schema files are loaded when the AtgiSchemaTypeLoader and ColladaResolver components are initialized while the application starts up. These schema definitions provide the information needed to parse and view model files governed by the data models defined in the schemas.

IResourceResolver.Resolve Method

Once the schema describing model files has been loaded into the application, ModelViewer can load model files.

The IResourceResolver.Resolve() method loads a file from a given URI and produces an object implementing IResource, a resource with a type and unique URI.

The DomNode class represents an object of one of the types described in the data model for ATGI or Collada. The Resolve() method creates a tree of DomNode objects representing all the data in the model file. Resolve() returns a DomNode that is the root of the DomNode tree.

The returned DomNode is adapted to IResource, as this code for ColladaResolver.Resolve() demonstrates:

public IResource Resolve(Uri uri)
{
    string fileName = PathUtil.GetCanonicalPath(uri);
    if (!fileName.EndsWith(".dae"))
        return null;

    DomNode domNode = null;
    try
    {
        using (Stream stream = File.OpenRead(fileName))
        {
            var persister = new ColladaXmlPersister(m_loader);
            domNode = persister.Read(stream, uri);
        }
    }
    catch (IOException e)
    {
        Outputs.WriteLine(OutputMessageType.Warning, "Could not load resource: " + e.Message);
    }

    IResource resource = Adapters.As<IResource>(domNode);
    if (resource != null)
        resource.Uri = uri;

    return resource;
}

Resolve() does the following:

  1. Checks that the file extension is "dae".
  2. Creates a DomNode.
  3. Reads file data with a Stream.
  4. Creates a ColladaXmlPersister, which derives from DomXmlReader, using the ColladaSchemaTypeLoader object. This loader has the information from the Collada schema.
  5. Call ColladaXmlPersister.Read() to convert the stream data into a tree of DomNode objects, with a DomNode at the root. The ColladaXmlPersister has the information to do this from the ColladaSchemaTypeLoader that loaded the Collada schema.
  6. Adapts the root DomNode to an IResource and returns it.
The AtgiResolver.Resolve() method performs similarly.

Loading the model file's data creates a tree of DomNode objects that the application can work with to render the model file's data.

ATGI and Collada Schema Classes

In addition to the type definition files, each model provides a Schema class, generated by the ATF DomGen utility from the XML schema definitions for the ATGI and Collada data models. DomGen creates a metadata class, such as DomNodeType, for every type in the schema definition file and provides a convenient way to reference the types in the two different data models. In particular, each DomNode has a DomNodeType metadata object associated with it, describing the node's type.

These metadata classes in the Schema classes are used when the application starts up by the document client component, ModelViewer, as part of its initialization. For details, see ModelViewer Class.

Document Handling

ModelViewer implements IDocument and IDocumentClient to handle documents in a class and a component. For general information about documents, see Documents in ATF.

ModelDocument Class

In ModelViewer, the ModelDocument class implements IDocument and holds the model data in a file. ModelDocument's constructor takes parameters for a DomNode and a URI:

public ModelDocument(DomNode node, Uri ur);

The DomNode here is the root of a tree of DomNode objects, because model data in the document is treated as a tree of DOM nodes, just as in the ATF DOM. This is discussed in Model Data Handling.

The ModelDocument constructor sets the RootNode property to this root DomNode, and this property is referenced wherever the model data is needed.

ModelViewer only reads documents and does not edit them, so these properties are set accordingly:

public bool IsReadOnly
{
    get { return true; }
}
...
public bool Dirty
{
    get { return false; }
    set { throw new InvalidOperationException(); }
}

IDocument derives from IResource, so it also defines the Type property to indicate the document type:

public string Type
{
    get { return "3D Model"; }
}

ModelViewer Class

The ModelViewer component is a document client, implementing IDocumentClient. Its constructor creates a DocumentClientInfo, obtained from the IDocumentClient.Info property. This determines the document type and extensions the application handles:

public ModelViewer()
{
    string[] exts = { ".atgi", ".dae" };
    m_info = new DocumentClientInfo("3D Model", exts, null, null, false);
}
...
DocumentClientInfo IDocumentClient.Info
{
    get { return m_info; }
}

ModelViewer Initialization

The ModelViewer component's initialization defines DOM extensions used for various DomNode types:

void IInitializable.Initialize()
{
    // Register ATGI and Collada nodes
    Register<RenderTransform>(Sce.Atf.Atgi.Schema.nodeType.Type);
    Register<RenderPrimitives>(Sce.Atf.Atgi.Schema.vertexArray_primitives.Type);

    Register<RenderTransform>(Sce.Atf.Collada.Schema.node.Type);
    Register<RenderPrimitives>(Sce.Atf.Collada.Schema.polylist.Type);
    Register<RenderPrimitives>(Sce.Atf.Collada.Schema.triangles.Type);
    Register<RenderPrimitives>(Sce.Atf.Collada.Schema.trifans.Type);
    Register<RenderPrimitives>(Sce.Atf.Collada.Schema.tristrips.Type);

    if (m_scriptingService != null)
        m_scriptingService.SetVariable("viewer", this);
}
...
private static void Register<T>(DomNodeType nodeType) where T : new()
{
    nodeType.Define(new ExtensionInfo<T>());
}

Defining a DOM extension on a type allows DomNode objects of this type to be adapted to other objects, whose API is more suitable to the task at hand. The classes here, RenderTransform and RenderPrimitives, are known as DOM adapters, and are discussed in Rendering DOM Adapters.

In most of the samples, DOM extensions are defined in the application's Schema loader. However, ModelViewer has no Schema loader, since it doesn't define its own data model. Instead, it uses the schemas defined for ATGI and Collada data. In particular, it uses metadata classes like Sce.Atf.Atgi.Schema.nodeType.Type, as shown in the IInitializable.Initialize() above. These metadata classes are in the Schema classes generated by DomGen, as discussed in ATGI and Collada Schema Classes.

ModelViewer CanOpen and Open Methods

ModelViewer only views model data; it doesn't change it, so the document client doesn't need to save data to a file or even close the file. Only open functions are needed; all the other methods in IDocumentClient do nothing.

CanOpen() simply checks that the file extension is appropriate:

bool IDocumentClient.CanOpen(Uri uri)
{
    return m_info.IsCompatibleUri(uri);
}

The Open() method creates a ModelDocument:

IDocument IDocumentClient.Open(Uri uri)
{
    foreach (IResourceResolver resolver in m_resolvers)
    {
        DomResource res = resolver.Resolve(uri) as DomResource;
        if (res != null)
        {
            return new ModelDocument(res.DomNode, uri);
        }
    }

    return null;
}

This Open() method, though also brief, requires some explanation.

First, the IResourceResolver objects are iterated from the field m_resolvers, which is imported from all components exporting IResourceResolver:

[ImportMany]
private IEnumerable<IResourceResolver> m_resolvers;

ModelViewer has two such IResourceResolver exporters: AtgiResolver and ColladaResolver, discussed in the Resource Resolvers section.

The loop attempts to resolve the URI using all the IResourceResolver objects it has. It calls Resolve() for each resolver and sees if it gets a non-null result, indicating resolution succeeded. Resolve() creates a tree of DomNode objects from the data in the model file. This tree is used to render the model into a 3D drawing.

Open() constructs a new ModelDocument using the URI and the DomNode tree's root, and then returns the ModelDocument.

Rendering Components

The resource resolver components AtgiResolver and ColladaResolver load a model file and create a tree of DomNode objects. Each DomNode represents an object that can be rendered. During the rendering process, these DomNode objects are adapted to various interfaces, such as IRenderObject, through which rendering ultimately occurs.

Two ModelViewer components facilitate model rendering.

RenderView Component

RenderView registers a DesignControl that is used to display a 3D scene from the active document.

Several classes are used to render objects. SceneNode objects hold all the render objects and constraints that are associated with an object being rendered. SceneNode objects are arranged in a graph that determines which objects to render, and in what order nodes are traversed during rendering. Scene derives from SceneNode and holds the root SceneNode of the scene graph.

SceneGraphBuilder builds a scene graph from a root source object, typically a DomNode. In the ModelViewer application, the scene graph is built from the DomNode tree. A SceneNode object holds a reference to its underlying source object, the DomNode.

The RenderView constructor creates two objects:

public RenderView()
{
    m_scene = new Scene();
    m_designControl = new DesignControl(m_scene);
}

DesignControl extends CanvasControl3D to provide scene graph rendering and picking, using the scene graph provided in its constructor's Scene parameter. DesignControl is the canvas on which model objects are drawn.

The IInitializable.Initialize() method for the RenderView component registers the DesignControl and subscribes to the active document changed event:

void IInitializable.Initialize()
{
    ControlInfo cinfo = new ControlInfo("3D View", "3d viewer", StandardControlGroup.CenterPermanent);
    m_controlHostService.RegisterControl(m_designControl, cinfo, null);

    m_documentRegistry.ActiveDocumentChanged += (sender, e) =>
    {
        ClearRenderGraph(m_context);
        m_context = null;

        ModelDocument doc = m_documentRegistry.GetActiveDocument<ModelDocument>();
        if (doc != null)
        {
            m_context = doc.RootNode;
            SceneGraphBuilder builder = new SceneGraphBuilder(typeof(IRenderThumbnail));
            builder.Build(doc.RootNode, m_scene);
            Fit();                                                    
        }
    };

}

A lambda expression is used for the event handler. In this expression, ClearRenderGraph() clears the DesignControl canvas, because a model is going to be rendered for the new active document. The handler creates a ModelDocument for the active document.

If the new document is valid, the lambda expression constructs a SceneGraphBuilder that builds a scene graph from the specified type of IRenderObject objects. In this case, objects to render must implement IRenderThumbnail, which is an interface for objects that can generate thumbnails. The IRenderThumbnail interface is empty, so this is not much of a restriction.

IRenderObject is an interface for renderable objects. IRenderObject extends IBuildSceneNode, which enables scene graph building for a DOM object. The nodes in the model's DomNode tree are adapted to IRenderObject in the process of rendering.

The SceneGraphBuilder.Build() method creates a scene graph from the DomNode tree. This scene graph is used for rendering a document's model in the DesignControl as long as that document is active.

The Fit() method fits the rendered object in the DesignControl window. It changes the settings of DesignControl's Camera to fit the object.

RenderCommands Component

The RenderCommands component provides user commands related to the RenderView component to change rendering mode.

Its IInitializable.Initialize() method registers commands with the CommandService component, which it imported into the m_commandService field:

public virtual void Initialize()
{
    m_commandService.RegisterCommand(
        Command.Fit,
        StandardMenu.View,
        CommandGroup,
        "Fit",
        "Fit All",
        Keys.F,
        null,
        CommandVisibility.Menu,
        this);
    m_commandService.RegisterCommand(
        Command.RenderSmooth,
        StandardMenu.View,
        CommandGroup,
        "Smooth",
        "Smooth shading",
        Keys.None,
        Resources.SmoothImage,
        CommandVisibility.All,
        this);
    ...
    m_commandService.RegisterCommand(
        Command.RenderCycle,
        StandardMenu.View,
        CommandGroup,
        "CycleRenderModes",
        "Cycle render modes",
        Keys.Space,
        null,
        CommandVisibility.Menu,
        this);

Commands are triggered by a menu item and/or a tool button, depending on the setting of the CommandVisibility parameter. Icons for menu items and tool buttons are specified in the icon parameter, which references items like Resources.SmoothImage. All these image resources are in the Resources class, which provides several sizes of each image so the right size can be used for a menu item or tool button.

RenderCommands implements ICommandClient, whose methods determine whether commands can be performed and performs them. CanDoCommand() returns true when the DesignControl exists.

The DoCommand() method performs all the registered commands:

public void DoCommand(object commandTag)
{
    if (commandTag is Command)
    {
        DesignControl control = m_renderView.ViewControl;
        switch ((Command)commandTag)
        {
            case Command.Fit:
                m_renderView.Fit();
                break;

            case Command.RenderSmooth:
                control.RenderState.RenderMode &= ~RenderMode.Wireframe;
                control.RenderState.RenderMode |= (RenderMode.Smooth | RenderMode.SolidColor |
                    RenderMode.Lit | RenderMode.CullBackFace | RenderMode.Textured);
                control.Invalidate();
                break;

In the Command.Fit case, the Fit() method fits the rendered object in the canvas.

Most of the other commands, such as Command.RenderSmooth, change how the model is rendered. RenderState is a platform-independent representation of a GPU render state. It uses the RenderMode enum, which corresponds to various rendering modes, such as wireframe. The commands here set a new RenderState for the control and then call Invalidate() to trigger repainting the model object in the DesignControl, which is discussed in DesignControl Operation.

Finally, the UpdateCommand() method updates the UI appearance based on the current rendering state. This results in the appropriate menu item being checked and tool button highlighted.

DesignControl Operation

After the scene graph is built, it is traversed by DesignControl to render the model. The actual rendering is done by methods called in the DOM adapters, RenderTransform and RenderPrimitives, discussed in Rendering DOM Adapters.

DesignControl's constructor sets up several objects to assist in the rendering process:

public DesignControl(Scene scene)
{
    m_scene = scene;

    m_renderAction = new RenderAction(RenderStateGuardian);
    m_pickAction = new PickAction(RenderStateGuardian);

    // default render states. These correspond to the state of the toggles on the toolbar,
    //  like wireframe on/off, lighting on/off, backface culling on/off, and textures on/off.
    m_renderState.RenderMode = RenderMode.Smooth | RenderMode.CullBackFace | RenderMode.Textured | RenderMode.Lit | RenderMode.Alpha;
    m_renderState.WireframeColor = new Vec4F(0, 0.015f, 0.376f, 1.0f);
    m_renderState.SolidColor = new Vec4F(1,1,1, 1.0f);
}

A RenderAction is created and default values for a RenderState object m_renderState are set. RenderAction implements IRenderAction, which features methods to build a traverse list from the scene graph and dispatch the list for rendering.

When a Paint event occurs for the DesignControl, such as when the view is invalidated, it calls its Render() method:

protected override void OnPaint(PaintEventArgs e)
{
    Render(m_renderAction, false, false);
    m_invalidated = false;
}

Render() calls RenderAction.Dispatch(), which calls BuildTraverseList() to build a traverse list of TraverseNode objects. TraverseNode is a class for encapsulating the rendering state for each IRenderObject object. BuildTraverseList() calls the IRenderObject.Traverse() method for each IRenderObject object in the scene graph to create a TraverseNode instance and add it to the traverse list, if the object is to be rendered. This results in the Traverse() method in RenderTransform and RenderPrimitive being called for each IRenderObject, as seen in Rendering DOM Adapters.

After creating the traverse list, RenderAction.Dispatch() calls RenderPass() to render the traverse list. This results in the Render() method in RenderPrimitive being called for each IRenderObject, so each node is rendered, which is commented on in RenderPrimitives DOM Adapter.

DesignControl uses a Camera object with default settings to display rendered objects.

Rendering DOM Adapters

Recall that the ModelViewer component's initialization defines DOM extensions for some DomNode types, as shown in ModelViewer Initialization. DOM extensions for ATGI types are seen here:

void IInitializable.Initialize()
{
    // Register ATGI and Collada nodes
    Register<RenderTransform>(Sce.Atf.Atgi.Schema.nodeType.Type);
    Register<RenderPrimitives>(Sce.Atf.Atgi.Schema.vertexArray_primitives.Type);
    ...

The result of these definitions is to allow DomNode objects to be adapted to another class. From the second definition above, for instance, a DomNode of type Sce.Atf.Atgi.Schema.vertexArray_primitives.Type is adapted to RenderPrimitives, a DOM adapter class. This means that all the methods and properties of RenderPrimitives can be used on this type's DomNode.

The ModelViewer application defines two DOM adapters, RenderTransform and RenderPrimitives. Note that:

  • Both these DOM adapters derive from RenderObject and thus implement IRenderObject. Thus, a DomNode object of any type for which these DOM extensions are defined implements IRenderObject. This is a prerequisite for being rendered, as mentioned in Rendering Components.
  • Both implement IRenderThumbnail, so SceneGraphBuilder can build a scene graph using any DomNode of types for which DOM extensions are defined. For details on SceneGraphBuilder and IRenderThumbnail, see RenderView Component.

RenderTransform DOM Adapter

This DOM adapter is used for "node" types in both ATGI and Collada that contain other nodes.

RenderTransform implements ISetsLocalTransform:

public class RenderTransform : RenderObject, IRenderThumbnail, ISetsLocalTransform

ISetsLocalTransform is an interface for IRenderObject objects that sets the local transform matrix (the transform from the parent to this render object) by calling IRenderAction.PushMatrix() in the Traverse() method.

The Traverse() method has this call:

action.PushMatrix(m_node.Transform, true);

The m_node field is set from the original DomNode, adapted to ITransformable, an interface for objects that maintain 3D transformation information:

m_node = this.Cast<ITransformable>();

m_node is a node with transformation information obtained from its Transform property, which contains a local transformation matrix. This transformation information comes from the original DomNode.

This node basically specifies that any of its child node renderable objects should be transformed using the given matrix.

To demonstrate what this means, this figure shows a model drawn using the transformations performed by this DOM adapter:

Now, suppose that this line is removed from the ModelViewer component defining the DOM extensions, so the transform doesn't occur:

Register<RenderTransform>(Sce.Atf.Collada.Schema.node.Type);

Here is the resulting model:

The propellor is now in the cockpit, which is very bad news for the pilot! This group of renderable objects was not transformed properly.

This change to the transformation matrix is undone in PostTraverse(), which is called after post visiting the SceneNode specified by the graph path, so the original transformation matrix state is restored:

action.PopMatrix();

RenderPrimitives DOM Adapter

This adapter actually does the work of rendering the model, using OpenGL®. This class also derives from RenderObject and implements IRenderThumbnail, but not ISetsLocalTransform because it does not change the transformation matrix:

public class RenderPrimitives : RenderObject, IRenderPick, IRenderThumbnail

The RenderPrimitives.Traverse() method is simpler than the version for RenderTransform. It mainly calls the base Traverse() method that creates a TraverseNode instance and adds it to the traverse list.

The Init() method initializes the render object, which happens once, when SceneGraphBuilder builds a scene graph in its Build() method, as described in RenderView Component.

The RenderPrimitives.Render() method is called to render the object.

Both Init() and RenderPrimitives.Render() actually use OpenGL® to perform the rendering. OpenGL®'s operation is beyond the scope of this discussion.

Topics in this section

Clone this wiki locally