Renovating a Collada viewer application – Part I

Introduction.

The community around Google SketchUp offers a very large number of 3D models, really good-looking, also and it is very easy to create new models as well. Several years ago I considered the idea to embed these models available in Collada (TM) format in any WPF applications. So I made a simple program in a very short time to convert this kind of models, so that they could be viewed in a WPF window.
The experimentation goal was reached, but the project stopped as it was.
Nowadays I am considering again the idea about the 3D for some new projects, but I have realized that the old Collada converter is too messy and rigid to be a serious component.
I take advantage of this to show you the renewal of this old program, by splitting the evolution on several parts, so that it will be clear how and where to operate any improvement. This is not because of the viewer itself, but it’s interesting to emphasize the usefulness of the good-practices on programming and to learn the use of the right tools instead.

The Collada format.

Collada is an acronym deriving from Collaborative Design Activity and it defines a file format which is able to describe whole 3D-scenes, even with animations and physics in the latest standard specifications.
I would avoid talking about the story and the tech specs of Collada, and I invite you to visit the related portal. Instead, from my viewpoint, I find useful to highlight that is a XML-based format. Moreover it is a schema very well-oriented toward the common 3D-engines modeling specs: by the way it was born just for this target.
Furthermore, it is not my goal to dig into the programming techniques around DirectX or OpenGL, but I consider that the reader has a minimum of knowledge about the Media3D section of WPF.

COLLADA and the COLLADA logo are trademarks of the Khronos Group Inc.

The first viewer.

By taking the old converter-viewer program, the very first thing you notice is that is a bunch of classes, all of them mixed together, which is making exactly what it is expected, no more, nor less.
It is a simple project for a WPF application, containing a parser, a converter and a 3D-viewer. Everything is somewhat bound together, where is much noticeable the mess instead the ability to keep the sections separated, for reuse and expanding capabilities.
There are several limitations also: the parser support is able to interpret only a small part of the specifications. Moreover the program must be feed with a compressed (zip) Collada source, that because often the source model includes some bitmap.
The comments are almost missing, and that makes the comprehension of the program sources very hard after years of latency.

So far, we may say that the program is working well, and has shown that taking advantage of the Collada models together with the WPF framework was targeted. However, we are quite far from considering this experience as something reusable and professional.
It is much like an experimentation with an electronic circuit on a bread-board: we are going to make it working fastly, but it is not a definitive application.
The logic data-flow is extremely simple: the Collada source file must be in a compressed (zip) form, and is parsed firstly as XML DOM. From this XML model, to the building of an intermediate Collada model, thanks to the “parser” section. The “builder” section takes the intermediate model and builds a 3D scene as WPF, by programmatically creating several Visual3D instances and grouping them as well.

The graphic presentation of the 3D object is realized by a normal Viewport3D control. There are also some helping tools to facilitate the three-dimensional viewing. I am not a 3D-editor expert, but I noticed that the interactivity way offered by Google SketchUp is particularly easy and effective. So I decided to take inspiration and mimic the navigation, giving the users three degree of freedom and the use of mouse to move.

The parser and the builder.

Ad stated above, it is necessary that the source Collada document must be in the compressed (zip) form, i.e. a compressed archive containing an XML document, together with any number of bitmaps used as textures. The extraction of these files from within the source archive is made automatically in memory, thanks to the wonderful SharpZipLib library, as part of the awesome SharpDevelop project.
The very first step is to load the XML document into an XLinq DOM. I prefer more the XDocument than the classic XmlDocument, because it is much more effective and straightforward, plus it is possible to take advantage of the XLinq. Before using C#, I have used XmlDOM (COM) in Visual Basic 6 for years, and it is a very good library anyway, especially when used in conjunction with XPath.

The intermediate Collada model is built by scanning the XML tree, and instantiating the classes upon a simplified schema. To do this, just invoke the static method LoadModel of the ColladaParser class: it returns a context, being the container of several resources realizing the overall intermediate Collada model.
At this point the role of the parser is over.
To generate the WPF 3D model, just call the static method CreateModel of the ColladaBuilder class. Such a function creates a ModelVisual3D instance, based on the intermediate Collada model. The final step is the insertion of that instance into the WPF viewport hosted by the application.

The most careful readers may have noticed an extra step, that could be avoided.
From the Collada XML source (so XDocument) there is a production of an intermediate Collada model, then of a definitive WPF model. It looks as unnecessary passing through an intermediate model; however it offers some easing and is faster to develop as an experiment.
The intermediate model does have a great advantage as well.
If we are supposing to operate starting from a relatively complex source, containing several bitmaps, could arise to a uselessly and costly processing when duplicating the same model many times on the target viewport. The bitmaps extraction itself has a cost, maybe not relevant, but by repeating several times the same operation is surely a waste of resources. Along this way the intermediate model is a concrete trick to avoid waste of CPU time, because the costly operations such as extraction and the textures composition are done only once, during the parsing phase. To replicate any time the WPF 3D model is a relatively cheaper task, done by the builder section, because it has only to “assemble” various parts together as a bunch of ModelVisual3D instances.

It is worth noting that the intermediate model could be managed by the builder with an option. During the 3D surface building pass, that property allows to choose whether to render both the front and the back face, instead of the front only. I was not able to understand how the Collada specs indicate this detail, so I decided to add the choice, and then let the user decide.

Any 3D surface is modeled with many triangular planar sections, called “texels”. Of course, being a planar section, any texel owns two faces. During the building pass, it is necessary to specify the orthogonal vector to the plane (at the triangle vertices). The vector versus along with the vertices sequence determines the “front” face, which is textured by default. The opposite (back) face is not considered normally, and in the viewport it would be seen as non-existent (i.e. fully transparent).

This peculiarity, if not managed properly, can carry to noticeable imperfections when rendered. That is the way I decided to insert an option. Fortunately the WPF libraries simplify a lot this kind of management, because in the GeometryModel3D class there is a property for the frontal “material”, and another one targeted for the back face as well.

The viewer.

The viewer itself is the Viewport3D control that is included in the WPF libraries. We are going to describe what kind of helpers has been built around that control, to ease the 3D object-moving interactivity between the human and the machine.
Must be noticed that this program does not allow to modify most of the parameters of the 3D scene, such as the lights and the background. The only thing the user does is to move the camera around the space, by using the mouse.
As briefly stated, the only way to interact with the program is the mouse, and that is a clear sign of limitation, because it could be useful the use of the keyboard, but the touch also wherever the monitor supports it.
The three fundamental functions allowed for the interaction are:

  • orbit
  • zoom
  • pan

The orbit movement let the user control the inclination of the camera respect to the “floor”.
The zoom moves the camera along the direction of the observer.
Finally, the panning allows the user to move the camera anywhere on the floor plane, since the Y-axis is considered orthogonal to that plane.

Even in this case are noticeable several limitations of the program. For example, it cannot be possible to spin the camera around the observer’s direction; also there is not any easing to move the camera around a particular point in the space (typically an observed point).
The most important limitation is that the parser takes no care about the axis-system declared by the Collada document: this brings often to an upside-down rendering of the model.
That was born as an experimental program and it gives pretty good result, but there are many features that should be added. It should be offered the ability to manage the lighting, to move any single model instead the camera, and so away.
All these gaps should be seen as an input for a dramatic and careful revision of the project, being able to fit easily to extensions and components reuse.
A surgeon cannot operate on a patient without having a clear idea to his problem.
It is necessary to start from a careful analysis of the current application, and then point some targets to reach, step-by-step for the future releases.

Code analysis with NDepend.

I want to emphasize once again the meaning of this series of articles: the real goal is how to write a good code, stable and reliable. It is absolutely reasonable thinking to an application being evolved and got more complex, but that cannot be an alibi to transform the source to a spaghetti-code, having hard time to maintain.
A very interesting tool built for the code analysis is NDepend.
NDepend performs easily several kinds of different analysis of our code, and assemblies as well, leveraging by a very smart core functionality: the CQL (Code Query Language). The CQL is the Columbus Egg’s for the code analysis, because it treats the sources of our projects as they were a database, where we may perform queries in a SQL-like fashion on it.
This simple-to-use, and effective also, engine performs the analysis of a large number of code metrics; it represents the code structure graphically in several ways, its complexity and even any cyclic dependency of classes. This is only an overview of the features of NDepend, and I invite you to browse them carefully on its home-site.
So far, it is interesting to test the Collada viewer code, so to understand where potential structural problems are.
NDepend installs as an add-in for the ordinary Visual Studio IDE, but it may be used as stand-alone for the Visual Studio Express users. The following picture shows the Visual NDepend main screen, being the stand-alone version.

To analyze our viewer code is straightforward: just pick “Analyze VS solutions” and browse for the proper Visual Studio solution.
The analysis process takes only few seconds, and terminates creating a useful HTML report, shown automatically in your favorite browser. Such a report synthesizes the overall result of the many CQL queries performed on the code, so that having at-a-glance an overview about the project structure.

The “CQL Rules summary” section summarizes the overall result of all the queries performed. It is noticeable how any project will be subject of over 100 different tests. In the specific case of our Collada viewer, there is not any critical error, but there have been listed 26 warnings.
At this point it becomes really interesting to dig deeper, just to understand where the problems are and how to solve them. To do this, let’s hide the report and take a look at the NDepend main screen.
This screen looks surely as a high-impact window, plenty of colored frames, but may be also scary for the novices. However it is an environment very well described, so you may gain familiarity briefly.
Let’s go step-by-step.
In the lower side of the NDepend screen there is the “Query explorer” pane, showing all the planned queries along with their result status.

In our test there are several warnings, even the overall result is not so bad. For example, there is a clear indication about the scarce quantity of comments, and a good documentation is a very important task to consider for the development process.

As seen above, each test performed is a CQL query. In the left pane of the NDepend window we may read how these queries are written, along with a brief description about the meaning and the expectations. From my viewpoint, CQL is an extremely powerful and versatile feature, but requires a bit of familiarity with the code-quality analysis common practices.

We do know already about the mess in the Collada viewer sources. Now it’s time to take a peek at the dependency matrix.

A colored box indicates a dependency between types of the related row and column. A blue box means that the column-type refers to the row-type; vice versa for a green box. The number indicates how many dependencies of such a pair of types are involved.

We may see that there are two rows/columns particularly dense of references than others (ColladaParser and ColladaParserContext). That is a good new, because indicates that most of the data are “flowing through” these classes. A certain separation between logical layers indicates pretty good abstraction; despite we are examining a single bunch of classes, offering several different functions.
The bad news is that: there are two cases of cyclic dependency. In other words, it means that two different types refer and depend to each other. This is something that should be avoided, because it makes a barrier toward a good abstraction, thus the separation and reuse of components.

The dependency matrix itself can be seen as a graph. Maybe this could look the most intuitive way to observe the classes’ dependencies, but it is also true that the graph becomes increasingly hard to read due the huge quantity of links.

At any time it is possible to choose a class (type) by clicking on it, so that it will be highlighted, as long its dependencies. Please note the cyclic-dependency cases on the graph, they having a hot-reddish bidirectional link connecting to.

It is also possible to view the complexity of the code structure by viewing the tree map.

Conclusions.

Next time we will see how to approach a similar Collada viewer application, bearing in mind flexibility, abstraction, as long as good-practices of programming. The goal is build a viewer able to grow as functionalities, without falling into a rigid implementation, expensive to maintain.

Here is the source of this application:
Highfield.ColladaViewer.doc
(Remember to change the .doc extension to .zip)

4 thoughts on “Renovating a Collada viewer application – Part I

    • Mario Vernari

      Hi Jeff!
      You app looks great, but I need something to embed into WPF applications, so .Net only.
      Anyway, I will keep your work in consideration for the future.
      Thank you so much.
      Cheers

  1. zproxy

    I failed to export and view COLLADA from Sketchup. Any hints?

    System.ArgumentException was unhandled
    Message=An item with the same key has already been added.
    Source=mscorlib
    StackTrace:
    at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
    at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
    at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
    at Highfield.ColladaViewer.ColladaLibraryNodesParser.ParseInstanceMaterial(ColladaParserContext context, XElement container, ColladaNode node) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryNodesParser.cs:line 187
    at Highfield.ColladaViewer.ColladaLibraryNodesParser.ParseTechniqueCommon(ColladaParserContext context, XElement container, ColladaNode node) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryNodesParser.cs:line 163
    at Highfield.ColladaViewer.ColladaLibraryNodesParser.ParseBindMaterial(ColladaParserContext context, XElement container, ColladaNode node) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryNodesParser.cs:line 142
    at Highfield.ColladaViewer.ColladaLibraryNodesParser.ParseInstanceGeometry(ColladaParserContext context, XElement container, ColladaNode node) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryNodesParser.cs:line 121
    at Highfield.ColladaViewer.ColladaLibraryNodesParser.ParseNode(ColladaParserContext context, XElement container) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryNodesParser.cs:line 65
    at Highfield.ColladaViewer.ColladaLibraryVisualScenesParser.ParseVisualScene(ColladaParserContext context, XElement container) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryVisualScenesParser.cs:line 63
    at Highfield.ColladaViewer.ColladaLibraryVisualScenesParser.Parse(ColladaParserContext context, XElement container) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaLibraryVisualScenesParser.cs:line 29
    at Highfield.ColladaViewer.ColladaParser.ParseStruct(ColladaParserContext context, XDocument document) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaParser.cs:line 121
    at Highfield.ColladaViewer.ColladaParser.LoadModel(ZipFile archive) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\Collada\Parser\ColladaParser.cs:line 61
    at Highfield.ColladaViewer.MainWindow.MenuItem_Click(Object sender, RoutedEventArgs e) in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\MainWindow.xaml.cs:line 399
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
    at System.Windows.UIElement.RaiseEvent(RoutedEventArgs e)
    at System.Windows.Controls.Primitives.ButtonBase.OnClick()
    at System.Windows.Controls.Button.OnClick()
    at System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs e)
    at System.Windows.UIElement.OnMouseLeftButtonUpThunk(Object sender, MouseButtonEventArgs e)
    at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget)
    at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent)
    at System.Windows.UIElement.OnMouseUpThunk(Object sender, MouseButtonEventArgs e)
    at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget)
    at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
    at System.Windows.UIElement.RaiseTrustedEvent(RoutedEventArgs args)
    at System.Windows.UIElement.RaiseEvent(RoutedEventArgs args, Boolean trusted)
    at System.Windows.Input.InputManager.ProcessStagingArea()
    at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
    at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport)
    at System.Windows.Interop.HwndMouseInputProvider.ReportInput(IntPtr hwnd, InputMode mode, Int32 timestamp, RawMouseActions actions, Int32 x, Int32 y, Int32 wheel)
    at System.Windows.Interop.HwndMouseInputProvider.FilterMessage(IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at System.Windows.Interop.HwndSource.InputFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
    at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
    at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
    at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
    at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
    at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
    at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
    at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
    at System.Windows.Application.RunDispatcher(Object ignore)
    at System.Windows.Application.RunInternal(Window window)
    at System.Windows.Application.Run(Window window)
    at System.Windows.Application.Run()
    at Highfield.ColladaViewer.App.Main() in Y:\opensource\unmonitored\highfield-colladaviewer\Highfield.ColladaViewer\Highfield.ColladaViewer\obj\x86\Debug\App.g.cs:line 0
    at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
    at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
    at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
    at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    at System.Threading.ThreadHelper.ThreadStart()
    InnerException:

    • Mario Vernari

      I stated that the program is not much robust, however…you should provide me the Collada source, so I’ll check for the error.
      Drop me an email with the XML source.
      Cheers

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s