Live Rendering of 3D Revit Element Geometry in a Remote WebGL Viewer

Today, I'll implement live real-time export of 3D geometry from a Revit add-in to a web-hosted WebGL viewer.

This is an enhancement to the initial version exporting 3D element geometry to a WebGL viewer, which just generated data that I copied and pasted to hard-code it into the NodeWebGL viewer as a proof of concept.

Meanwhile, I enhanced the WebGL viewer in various ways to prepare it for this real-time live connection, mainly by adding a REST API and support for HTTP POST:

Now that the viewer sports a REST API accepting POST data, the time has come to drive it directly from the Revit add-in instead of copy and paste of hard-coded geometry data into the viewer.

Here are the steps I took to achieve this:

Package Geometry Data into JSON

I retrieve the geometry to be rendered by analysing the element geometry of a selected Revit element and generating lists of face vertices, normal vectors and indices to represent it.

In the initial implementation, I just printed this data to the debug console for manual copy and paste to the viewer.

Now I package that data into a JSON string for rendering via the DisplayWgl method like this:

  List<int> faceIndices = new List<int>();
  List<int> faceVertices = new List<int>();
  List<double> faceNormals = new List<double>();

  . . .

  // Scale the vertices to a [-1,1] cube 
  // centered around the origin. Translation
  // to the origin was already performed above.
 
  double scale = 2.0 / FootToMm( MaxCoord( vsize ) );
 
  string sposition = string.Join( ", ",
    faceVertices.ConvertAll<string>(
      i => ( i * scale ).ToString( "0.##" ) ) );
 
  string snormal = string.Join( ", ",
    faceNormals.ConvertAll<string>(
      f => f.ToString( "0.##" ) ) );
 
  string sindices = string.Join( ", ",
    faceIndices.ConvertAll<string>(
      i => i.ToString() ) );
 
  Debug.Print( "position: [{0}],", sposition );
  Debug.Print( "normal: [{0}],", snormal );
  Debug.Print( "indices: [{0}],", sindices );
 
  string json_geometry_data =
    "{ \"position\": [" + sposition
    + "],\n\"normal\": [" + snormal
    + "],\n\"indices\": [" + sindices
    + "] }";
 
  Debug.Print( "json: " + json_geometry_data );
 
  DisplayWgl( json_geometry_data );

With that in hand, it is time for the exciting stuff.

Send an HTTP POST Request and Display Result in Browser

The DisplayWgl implements the following tasks:

The implementation looks like this:

  /// <summary>
  /// Toggle between a local server and
  /// a remote Heroku-hosted one.
  /// </summary>
  static public bool UseLocalServer = false;

  . . .

  /// <summary>
  /// Invoke the node.js WebGL viewer web server.
  /// Use a local or global base URL and an HTTP POST
  /// request passing the 3D geometry data as body.
  /// </summary>
  void DisplayWgl( string json_geometry_data )
  {
    string base_url = UseLocalServer
      ? "http://127.0.0.1:5000"
      : "https://nameless-harbor-7576.herokuapp.com";
 
    string api_route = "api/v2";
 
    string uri = base_url + "/" + api_route;
 
    HttpWebRequest req = WebRequest.Create( uri ) as HttpWebRequest;
 
    req.KeepAlive = false;
    req.Method = WebRequestMethods.Http.Post;
 
    // Turn our request string into a byte stream.
 
    byte[] postBytes = Encoding.UTF8.GetBytes( json_geometry_data );
 
    req.ContentLength = postBytes.Length;
 
    // Specify content type.
 
    req.ContentType = "application/json; charset=UTF-8"; // or just "text/json"?
    req.Accept = "application/json";
    req.ContentLength = postBytes.Length;
 
    Stream requestStream = req.GetRequestStream();
    requestStream.Write( postBytes, 0, postBytes.Length );
    requestStream.Close();
 
    HttpWebResponse res = req.GetResponse() as HttpWebResponse;
 
    string result;
 
    using( StreamReader reader = new StreamReader(
      res.GetResponseStream() ) )
    {
      result = reader.ReadToEnd();
    }
 
    string filename = Path.GetTempFileName();
    filename = Path.ChangeExtension( filename, "html" );
 
    using( StreamWriter writer = File.CreateText( filename ) )
    {
      writer.Write( result );
      writer.Close();
    }
 
    System.Diagnostics.Process.Start( filename );
  }

I wonder whether there might be an easier or more efficient way to transfer the HTTP result to the browser than saving it to a local file.

There are probably more efficient methods, but this approach is hard to beat for easy of use.

Install Node.js on Windows

For local testing, I initially thought I would run the node.js server on the Mac and access that from the Windows virtual machine.

However, I was unable to access the Mac localhost running my local node server from Parallels.

Happily, it took one single click to install node.js on the Windows virtual machine and then run the unmodified code inside the virtual box:

Just go to nodejs.org and click the big green Install button.

After doing so, I could immediately run the existing server locally inside the box:

NodeWebGL running locally on Windows

It worked fine locally.

Next, I switched to the remote viewer.

That worked as well with no problem.

Demo

Here is a quick one-minute video showing the TwglExport Revit add-in and the Heroku-hosted node.js WebGL server NodeWebGL in concerted action:

Download

The entire TwglExport source code, Visual Studio solution and add-in manifest are provided in the TwglExport GitHub repository, and the version presented here is release 2015.0.0.3.

The complete node server implementation is available from the NodeWebGL GitHub repo, and the version discussed here is 0.2.7.

Next Steps

The Revit element traversal is currently totally simplistic.

It grabs the first non-empty solid it can find and renders that with no questions asked.

This will not work for a family instance, for instance – please pardon the pun – to access any solid contained in its geometry, you have to navigate through the geometry instance level first.

I could obviously enhance the geometry traversal, as we already did for numerous other add-ins, e.g. the OBJ exporter.

That would be silly, though.

It is much easier to implement a custom exporter and grab the geometry from that.

No more worries about elements, transformations, instances and all that stuff.

Stay tuned and have fun!