OBJ Model Export Considerations

As I already said, I am

getting going with the cloud
,
and my first project is to export a Revit model to the

Wavefront OBJ
file
format, which seems to be pretty standard and compact.

My exploration so far led to a number of observations and other issues:

Cloud and Mobile DevBlog

Talking about cloud and mobile, we have another addition to our growing list of ADN DevBlogs:

The

Cloud and Mobile Development DevBlog
has now been launched.

For more background information on it and an overview of the other ADN DevBlogs, please refer to

Kean’s announcement
.

Selecting Model Elements

Before we can start exporting the Revit model geometry, we need to determine which elements to extract the data from.

We took a couple of stabs at this in the past; an early one was

for Revit 2010

(revisited),
selecting each element that:

  • Is not a family or a family symbol
  • Has a valid category
  • Has non-empty geometry

This proved to be too simple, and

further tests
were required to eliminate unwanted elements.

We revisited the topic using filtered element collectors to

retrieve all model elements in Revit 2011

(updated).

Here are some other possible criteria to apply besides the three listed above:

  • Category HasMaterialQuantities property
  • FilteredElementCollector WhereElementIsNotElementType and WhereElementIsViewIndependent methods

You may also find that you need to define detailed lists of categories or other criteria to satisfy for your specific requirements in selecting the elements to export.

OBJ File Format

Back to the OBJ format, one of its main advantages is its extremely succinct

file format specification
.
As you can see, the only data that absolutely needs to be specified is the vertices and faces.
Optionally, texture coordinates and normals can also be defined.
The slightly more extensive

Wikipedia version
also
explains how parameter space vertices can be specified, and how colours are defined using a material library.

Here is a sample OBJ file
cube.obj representing
a cube and a material library
material.mtl that
I found in the

.obj Loader for Android
distribution.

The cube definition is rather sub-optimal, though, listing and making use of 24 vertices, although we all know that a cube only has a total of eight corners.
Here is an optimised OBJ representation of a cube:


v 1 1 1
v 1 1 -1
v 1 -1 1
v 1 -1 -1
v -1 1 1
v -1 1 -1
v -1 -1 1
v -1 -1 -1
f 1 3 4 2
f 5 7 8 6
f 1 5 6 2
f 3 7 8 4
f 1 5 7 3
f 2 6 8 4

Eliminating Duplicate Vertices

As the trivial example above shows, you can significantly reduce the size of an OBJ by eliminating duplicate vertex definitions.
<!–
file smaller and you know, a cube has six faces, so the last section of the file listing exactly six faces makes sense:


f 1//1 2//2 3//3 4//4
f 5//5 6//6 7//7 8//8
f 9//9 10//10 11//11 12//12
f 13//13 14//14 15//15 16//16
f 17//17 18//18 19//19 20//20
f 21//21 22//22 23//23 24//24

The normal vectors are presumably superfluous, since a cube has planar faces anyway, so every sensible renderer will default to the correct normal vector pointing straight out of the face anyway.

Strangely enough, though, the faces are refencing 24 different points.
As we all know, a cube has 8 distinct vertices, not 24.
–>
The generator of the larger cube sample file above defined 24 points, i.e. 4 * 6.
It probably simply iterated over all six cube faces and output a new vertex definition for each face corner encountered, regardless of repetition.

This is very wasteful.
In Revit it also incurs the additional risk of defining vertices which are supposed to specify corners of adjacent faces, but may be slightly off, as we discussed in obtaining the

top faces of sloped walls
,

retrieving unique geometry vertices
from
a selected element, and most recently in the

geometry traversal to retrieve unique vertices
from
concrete structural elements.

If we collect all unique vertices in the Revit model and use the resulting list to avoid any duplicate definitions in the OBJ file, we can drastically reduce the number of vertex definitions, in this case from 24 to 8, and improve precision at the same time.

XYZ Vertex Lookup

According to the observations above, I implemented a vertex lookup class providing a method AddVertex taking an XYZ input argument specifying a new triangle vertex to store for exporting.

It looks up the given XYZ point and either returns the index of a previously defined point, if possible, or creates a new entry for it and returns that index.

As discussed so many times in the past, the point comparison has to include some fuzz.

Here is the full implementation of my VertexLookupXyz class with its own built-in XyzEqualityComparer:


/// <summary>
/// A vertex lookup class to eliminate duplicate 
/// vertex definitions
/// </summary>
class VertexLookupXyz : Dictionary<XYZ, int>
{
  #region XyzEqualityComparer
  /// <summary>
  /// Define equality for Revit XYZ points.
  /// Very rough tolerance, as used by Revit itself.
  /// </summary>
  class XyzEqualityComparer : IEqualityComparer<XYZ>
  {
    const double _sixteenthInchInFeet
      = 1.0 / ( 16.0 * 12.0 );
 
    public bool Equals( XYZ p, XYZ q )
    {
      return p.IsAlmostEqualTo( q,
        _sixteenthInchInFeet );
    }
 
    public int GetHashCode( XYZ p )
    {
      return Util.PointString( p ).GetHashCode();
    }
  }
  #endregion // XyzEqualityComparer
 
  public VertexLookupXyz()
    : base( new XyzEqualityComparer() )
  {
  }
  /// <summary>
  /// Return the index of the given vertex,
  /// adding a new entry if required.
  /// </summary>
  public int AddVertex( XYZ p )
  {
    return ContainsKey( p )
      ? this[p]
      : this[p] = Count;
  }
}

The XYZ points that I consider here are in the original Revit model coordinate system, i.e. in feet.

Integer-based Vertex Lookup

Thinking about the issue a bit further, though, I discovered several enhancement possibilities.

On one hand, it might be more useful for me to create the OBJ files in millimetres instead of feet.

Secondly, if you don’t require floating-point precision, using integers is always more efficient, and mostly simpler as well.

This led me to define my own integer-based point class for storing the vertex coordinates in millimetres instead of feet, and in whole numbers instead of floating point.

The rest of my algorithm remained completely unchanged, and the resulting OBJ file also only differed in the actual vertex coordinate values.

Here is the implementation of my VertexLookupInt class with its own built-in integer based point class PointInt and PointIntEqualityComparer:


class PointInt : IComparable<PointInt>
{
  public int X { get; set; }
  public int Y { get; set; }
  public int Z { get; set; }
 
  const double _feet_to_mm = 25.4 * 12;
 
  static int ConvertFeetToMillimetres( double d )
  {
    return (int) ( _feet_to_mm * d + 0.5 );
  }
 
  public PointInt( XYZ p )
  {
    X = ConvertFeetToMillimetres( p.X );
    Y = ConvertFeetToMillimetres( p.Y );
    Z = ConvertFeetToMillimetres( p.Z );
  }
 
  public int CompareTo( PointInt a )
  {
    int d = X - a.X;
 
    if( 0 == d )
    {
      d = Y - a.Y;
 
      if( 0 == d )
      {
        d = Z - a.Z;
      }
    }
    return d;
  }
}
 
/// <summary>
/// A vertex lookup class to eliminate duplicate 
/// vertex definitions
/// </summary>
class VertexLookupInt : Dictionary<PointInt, int>
{
  #region PointIntEqualityComparer
  /// <summary>
  /// Define equality for integer-based PointInt.
  /// </summary>
  class PointIntEqualityComparer : IEqualityComparer<PointInt>
  {
    public bool Equals( PointInt p, PointInt q )
    {
      return 0 == p.CompareTo( q );
    }
 
    public int GetHashCode( PointInt p )
    {
      return (p.X.ToString()
        + "," + p.Y.ToString()
        + "," + p.Z.ToString())
        .GetHashCode();
    }
  }
  #endregion // PointIntEqualityComparer
 
  public VertexLookupInt()
    : base( new PointIntEqualityComparer() )
  {
  }
  /// <summary>
  /// Return the index of the given vertex,
  /// adding a new entry if required.
  /// </summary>
  public int AddVertex( PointInt p )
  {
    return ContainsKey( p )
      ? this[p]
      : this[p] = Count;
  }
}

Next steps

I would dearly love to complete the entire discussion right here and now, but I guess that is enough for one go.

Here is an example of how far I have come with my implementation so far, exporting the rac_advanced_sample_project.rvt to OBJ and viewing it in a Windows-based

mesh viewer
,
still in mono-chrome, I’m afraid:

Sample model OBJ file

Here are some of the next steps I plan to take in the coming days:

  • Discuss the rest of the OBJ export implementation in its current state
  • Add support for colour
  • Upload to the cloud
  • View on mobile device

It will be interesting to see where this leads us.
As always, feedback and suggestions are more than welcome.


Comments

8 responses to “OBJ Model Export Considerations”

  1. Hi Jeremy,
    some remarks:
    OST_Planting has no material quantities, so if you omit elements of this category, you won’t export trees, for example.
    When iterating recursively through FamilyInstances’ geometry, through their Subcomponents etc., it may be useful to limit the iteration depth.
    Why getting the geometry of each door knob…?
    If any face has regions, you should query those sub-faces because they may have different materials.
    People usually split faces to assign other materials to them, I think.
    It may be senseful if you transform the geometry by centering it, depending on the viewer you plan to use.
    Cheers,
    Rudi

  2. Hi Jeremy,
    another point:
    If the element is a FamilyInstance, you should check its parameters.
    If one of them is of ParameterType.FamilyType, you may find the relating element (param.AsElementId etc.) and get its geometry, too.
    Bye,
    Rudi

  3. Hi again,
    you wrote:
    “Add support for colour”.
    Think of windows.
    You will also need transparency values.
    ;-)
    Cheers,
    Rudi

  4. Dear Rudi,
    Thank you!
    Good suggestion!
    I don’t really care that much, though. I do want the building to look recognisable, but not aim for full-blown photo-realistic rendering, so I think I’ll skip that one.
    Cheers, Jeremy.

  5. Dear Rudi,
    Hmh? I don’t quite understand the purpose of this?
    Cheers, Jeremy.

  6. Dear Rudi,
    I think I will happily skip plants for this particular exercise.
    I am not explicitly iterating into any family instances, just grabbing the top level geometry returned to me by the Revit get_Geometry call on the elements traversed by the collector. Since I am not recursing into anything, I do not see where to apply the limitation that you suggest.
    Your suggestions about handling split faces and transforming the geometry definitely make sense and should be kept in mind.
    Again, happily, this is just an exercise to get something recognisable out there and into the cloud for consumption on mobile devices, so I’ll try hard to keep it simple and leave the frills to you guys.
    Cheers, Jeremy.

  7. Hi Jeremy,
    since we cannot access material textures, there is no chance to create a photo-realistic rendering.
    Sadly.
    Bye,
    Rudi

  8. Hi Jeremy,
    for example, if my door FamilyInstance has a door knob which is determined by a Family Type parameter, I get the knob’s geometry this way.
    But it may be that this is unnecessary, so I tend to MyComment.Rollback()…
    Additionally, we can omit door knobs at all.
    Too much details ;-)
    Bye,
    Rudi

Leave a Reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading