Happy Friday! This week I am excited to introduce Andreas Brake as the guest writer for this post. Andreas was my partner in crime for our project StreamVR which won the Best Overall Project Award at the ENGworks Hackathon. 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 dive into the technical details of our VR application and how we set up communication between the RevitAPI and StreamVR.
To start with, I will discuss how we communicated information between the Revit plug-in and the VR Application. There were many options to weight when making this decision. We first looked at the option of running an HTTP server from the Revit plug-in that would act primarily as a proxy for the RevitAPI. This setup could allow us to perhaps even go so far as to expose the RevitAPI via GraphQL or other information retrieval frameworks popular in the web world. We ran quickly into a few problems with this. Firstly, due to corporate computer policies, it was not guaranteed (in fact very unlikely) that the Revit application would have sufficient permissions to expose HTTP ports on the machine and allow incoming traffic. Furthermore, we wanted to maintain the possible use case of having an architect or designer drive a meeting with clients which would require multiple applications connected to the model and staying in sync if any changes occurred. This scenario became dramatically more complex using HTTP calls.
Therefore, we decided to communicate our data over a message bus. Simply put, this is a program that allows for applications to push bits of information and have other connected applications listen to and process these messages. There are many options for messaging buses from MSMQ to RabbitMQ but we ended up using NATS as, among other reasons, it is a very light-weight solution that is deployable on most operating systems.
Using a messaging bus allows us to communicate between Revit and our Application as follows. We first stand up a nats-server on a machine that allows it. This means that a Revit user who has some admin privileges can run this locally. Alternatively, in a corporate setting, the organization’s IT team can deploy this to the local network and ensure a fixed IP or DNS to this server. Next, both the Revit plug-in and the VR Application connect to the server’s address. The plug-in subscribes to the TO_SERVER channel and the Application pushes requests and commands to this channel which the plug-in is now able to process and reply to.
VR is a powerful tool to convey design intent. Architects, clients, designers, contractors, and engineers can utilize VR to simulate use cases and to make decisions while experiencing the space. Although there are tools to view BIM (building information models) in VR, so far, the process has been linear, exporting a model from Revit into a rendering software. There is a disconnect between VR and BIM. By allowing users to make changes in VR and synchronize their changes back to Revit, design teams and owners can have more fluid conversations allowing design and documentation to exist in the same space.
The reason for the application hanging is because the plug-in is constantly looping and listening for new additions to the message queue. Every loop, it will attempt to pop a command from the queue and process it based on the message contents. These requests will generally return one or more RevitAPI objects that have been converted to a data transfer object (DTO) which can then be serialized and understood by our VR Application. These DTO schemas will be further discussed shortly. There are six currently supported request types and seven transfer objects.
Data Transfer Objects (DTOs) are the intermediate link between Revit and the VR application. Our Revit plug-in takes the features of a Revit object and saves these as parameters of a DTO and then can translate those parameters to the VR application and vice versa. Most of our DTOs inherit a simple Element class similar to the RevitAPI. Our base Element however only has 2 fields: Id and Name. Some of our objects, such as Family and Material, do not currently have additional data beyond these fields. For these cases, the Name field is being used to resolve an existing locally defined Prefab or Material in the VR application. The Id is then used to link these Unity GameObjects to FamilyInstances and material-defining Faces.
More complex objects include Wall, Floor, and Ceiling which are similar to each other as, beyond their inheritedId and Name fields, they primarily define a list of Faces that represent their geometry. This was the simplest way of telling our VR Application how to render these objects. The Application reconstructs the meshes based on the provided vertices and indexes.
The final DTO we define is the FamilyInstance. This object has the additional fields of a HostId, FamilyId, and a Transform object, defining the object’s position and rotation. Our focus for this hackathon was to provide the option to move, rotate, and place this DTO through the VR application.
Return: All objects of given type
Description: This request first takes the passed "Type" and uses Reflection to resolve the c# Type from the RevitAPI assembly. If the Type correctly resolves, we then use a FilteredElementCollector to get all objects of this Type from the Revit model which we then attempt to convert to the corresponding DTO.
Return: Object with corresponding ElementId
Description: This request differs from the GET_ALL request as it only takes the ElementId for the RevitAPI object. This ElementId is used with the Document.GetElement method to retrieve the corresponding object which is then converted to its DTO and returned.
Return: Updated DTO
Description: The set request takes in the entire DTO as a parameter. First, this process takes the "Id" field and resolves the corresponding RevitAPI object. Then, it creates a transaction and passes both of these objects through the defined reverse mapper for the object’s Type. The fields that are able to be updated depend on what we support through the reverse mapper. Some fields are strictly read-only (e.g. Id) and therefore will not be updated even if the corresponding DTO field has a new value. In order to ensure that the VR Application has the correct values after this operation, the updated RevitAPI object is converted to its DTO and returned.
Return: Updated DTO
Description: This request is similar to SET as it also takes in a DTO to be updated. The primary difference comes in that the only DTO type supported is a "Face" which corresponds to the paintable Autodesk.Revit.DB.Face object. The process here is very straight-forward. The MaterialId and ElementId fields are extracted from the DTO and used to resolve the RevitAPI objects corresponding to the new material and parent object for the face. Then, the exact face of the parent is identified using the provided FaceIndex. The new material is then applied using the existing Document.Paint method. As usual, the updated face DTO is returned.
Param: Base object details
Return: DTO for created object
Description: Create operates differently than our previous requests as it needs to create a new RevitAPI object based on base fields. Which fields are required for input depends on the Type to be created. For this hackathon we focused on support for creating new FamilyInstances. The fields required for FamilyInstances are the FamilyId and the Transform.Origin location where the object should be placed. The object is then placed using the Document.Create.NewFamilyInstance method.
Description: This final parameter is very simple. It tells the plug-in to stop processing requests and return control to the Revit Application.
And that sums up the work we did for the hackathon. Another huge thank you to Andreas Brake for guest writing this post. I hope you learned a few things to consider for your future projects. Check out our demo video and github to view the work we did! Next week, I will dive back in to the Revit plug-in for randomizing curtain walls.