Happy Friday! This week I am excited to welcome back Andreas Brake as the guest writer for this post. He is a software engineer at Topgolf Media doing server development. In his free time, he enjoys hiking, gaming, and gardening. This week, he will discuss how to export Revit geometry as OBJ files using C#, a feature we used when working on StreamVR, a Hackathon project we did in August.
One exciting usage of Revit plugins is to add interoperability between Revit and external programs. This can take many forms, but sometimes you may want to transfer geometry from Revit including Families in the model. This presents some difficulty, as Revit allows exporting Families as FBX files or other proprietary formats but lacks a built-in export to a widely used open format such as OBJ. This post will be describing what an OBJ model looks like and how to create one in C# using the Revit API.
Simply put, an OBJ file is a plain-text file that contains structured information that describes some arbitrary geometry. In its most basic form, this file will define a series of vertices and faces that can be read and reconstructed into a 3D model. The example below defines a Cube.
Click Here to download this example OBJ. This download is generated in-browser so no data will be sent to any server.
In addition to these simple parameters, an OBJ file can be extended with information for applying correct normal vectors and texture mapping among many other features. This post will consider the more basic features for brevity, but further examples of OBJ files can be found here.
Beyond geometry information, an OBJ file can also contain organizational lines that allow for sub-geometry to be defined. These organizational groups can also be given names that can be read by external model editors.
Because an OBJ file only contains plain-text information, it becomes a good option for interoperability as it is then easier to serialize and transmit as a file, over HTTP, or really any other channel for communication between programs.
Our code starts by assuming that the program has access to the current Document and an ElementId for a FamilySymbol. To review: a "Family" that is placed in a Revit Model is represented in code as a FamilyInstance object which contains positional, rotational, and other instance data. This object is a child of a FamilySymbol object. A FamilySymbol defines the Geometry for a "Family" and is a shared parent for all FamilyInstances of the same type. Beyond this, there exists a true Family object that is the parent of one or more FamilySymbols and can act as a container for variations of similar FamilySymbols. This post will only focus on and interact with FamilySymbols.
The high-level structure of our function looks like this:
Here, we get the FamilySymbol and iterate over each GeometryObject associated with it. GeometryObjects can represents various types of 1D, 2D, and 3D forms, but we will consider only Solids. We start our OBJ file data by creating a StringBuilder which will help us build out longer strings than would be reasonable using simple string concatenation. This StringBuilder will then have appended to it our first line of the file. This is an "o <name>" line that tells any reading program that the following lines will contain geometry for the <name> object.
For each Solid, we will write a "g <name>" line that will tell the reader to consider each face as a separate grouping. This is technically optional, but can help for model organization.
This block is where we actually write the bulk of our file’s contents. First, we iterate over the Vertices member of the Face's Mesh and write each of these to the file as a "v <x> <y> <z>" line. Note that sometimes, you may have to swap the Y and Z coordinates to orient the output model correctly.
After we have the vertices, we generate the triangles that will define our geometry. Luckily for us, Revit exposes some nice methods to help with getting these triangles. We can first iterate over the total number of triangles in the mesh and process each one individually. In order to create a "f <v1> <v2> <v3>" line, we need the indexes of the vertices as they appear in the OBJ file. This means that we have to keep track of the global vertex index using an "indexOffset" variable. Between this and the index, as retrieved from the MeshTriangle, we can compute a global index value for the desired triangle vertex and write these to the file.
Finally, we increment our various counters to track offsets and face numbers across face and part iterations.
After we have our full StringBuilder object, we can write this OBJ to a file like so:
We are writing to the system’s Temp path for ease of access, but an actual destination is up to you as the coder. Because the OBJ is simply a string, there are other options than writing to a file. It could also just as easily be sent over HTTP to a remote server or used elsewhere in the program.