A cross-platform library that allows you to transfer BIM data directly into your application and maintain a live link between them.
Video Demonstration
RevitToUnigineAndUnity.mp4
Vertex Flow is designed to speed up your building information modeling (BIM) workflows. No longer necessary to juggle multiple tools and take dozens of steps to prepare and optimize BIM data for creating 3D experiences.
Vertex Flow SDK can help you create real-time BIM applications by building on top of any 3D engine.
Vertex Flow makes the process of bringing BIM models into real-time 3D extremely simple by unlocking the ability to stream data directly into your application.
The first integration is with Revit, but you can integrate with any Autodesk products, as well as other industry tools.
- Install the latest version of Azure Cosmos DB Emulator
- Start the Azure Cosmos DB Emulator
- Install Revit 2022
- Open the
VertexFlow.addinfile and replace theAssemblysection with your output directory - Open the
VertexFlow.RevitAddin.csprojfile and replace theDestinationFolderin the targetAfterBuildsection
Once you build the solution and launch Revit, you should find the Vertex Flow pannel in the Add-Ins tab.
Note: If you don't want to install Revit, just unload the
src/VertexFlow.RevitAddinproject from the solution.
-
Create
Assembly Definitionin theAssets/Plugins/VertexFlowfolder withVertexFlowname -
Copy following libraries to the
Assets/Plugins/VertexFlow/libsdirectory- VertexFlow.Core.dll
- VertexFlow.SDK.dll
- VertexFlow.SDK.Extensions.dll
- VertexFlow.SDK.Listeners.dll
-
Create
signalr.ps1file in theAssets/Plugins/VertexFlow/libsfoldersignalr.ps1
$srcVersion = "3.1.20" $stjVersion = "4.7.2" $target = "netstandard2.0" $outDir = ".\temp" $pluginDir = ".\" nuget install Microsoft.AspNetCore.SignalR.Client -Version $srcVersion -OutputDirectory $outDir nuget install System.Text.Json -Version $stjVersion -OutputDirectory $outDir $packages = Get-ChildItem -Path $outDir foreach ($p in $packages) { $dll = Get-ChildItem -Path "$($p.FullName)\lib\$($target)\*.dll" if (!($dll -eq $null)) { $d = $dll[0] if (!(Test-Path "$($pluginDir)\$($d.Name)")) { Move-Item -Path $d.FullName -Destination $pluginDir } } } Remove-Item $outDir -Recurse
-
Check if NuGet CLI is installed locally
-
Execute the following command in PowerShell from the
Assets/Plugins/VertexFlow/libsdirectory to import the target .dll files./signalr.ps1
You will find a complete example in the
samples/VertexFlow.SDK.Sampleproject.
First of all, create a class to store mesh data.
using VertexFlow.Core.Models;
public struct CustomVector3
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
public class CustomMesh : MeshData<CustomVector3>
{
}You can use default
Vector3fromVertexFlow.Core.Structs
IMeshFlow<TMeshData> provides methods for adding SendAsync or replacing UpdateAsync existing meshes in a database.
static async Task Main()
{
using var vertexFlow = new VertexFlow("https://localhost:5001");
var meshFlow = vertexFlow.CreateMeshFlow<CustomMesh>();
var meshData = GetMeshData();
// Adds the mesh data to a database.
// Note: Will fail if there already is a mesh data with the same id.
await meshFlow.SendAsync(meshData);
// Updates the mesh data in a database.
// Note: Will add or replace any mesh data with the specified id.
await meshFlow.UpdateAsync(meshData.Id, meshData);
}
private static CustomMesh GetMeshData()
{
...
}IMeshStore<TMeshData> provides methods for reading GetAsync, GetAllAsync, or deleting DeleteAsync existing meshes from a database.
static async Task Main()
{
using var vertexFlow = new VertexFlow("https://localhost:5001");
var meshStore = vertexFlow.CreateMeshStore<CustomMesh>();
foreach (var mesh in await meshStore.GetAllAsync())
{
Console.WriteLine($"Mesh '{mesh.Id}' downloaded.");
}
}IMeshFlowListener invokes MeshCreated or MeshUpdated in response to mesh data changes in a database.
using VertexFlow.SDK.Listeners;
static async Task Main()
{
using var vertexFlow = new VertexFlow("https://localhost:5001");
using var meshFlowListener = vertexFlow.CreateMeshFlowListener();
// Occurs when new mesh data has been added to a database.
meshFlowListener.MeshCreated += (sender, meshId) =>
{
Console.WriteLine($"Mesh '{meshId}' created.");
};
// Occurs when new mesh data has been updated in a database.
meshFlowListener.MeshUpdated += (sender, meshId) =>
{
Console.WriteLine($"Mesh '{meshId}' updated.");
};
// Starts listening for changes in a database.
await meshFlowListener.StartAsync();
...
// Stops listening for changes in a database.
await meshFlowListener.StopAsync();
}Use VertexFlow.SDK.Extensions to automate the process of loading mesh data on adding OnMeshCreated or updating OnMeshUpdated a database.
using System.Net.Http;
using VertexFlow.SDK.Listeners;
using VertexFlow.SDK.Extensions;
static async Task Main()
{
using var vertexFlow = new VertexFlow("https://localhost:5001");
var meshStore = vertexFlow.CreateMeshStore<CustomMesh>();
using var meshFlowListener = await vertexFlow
.CreateMeshFlowListener()
.WithStore(meshStore)
.OnMeshCreated(mesh => Console.WriteLine($"Mesh '{mesh.Id}' created."))
.OnMeshUpdated(mesh => Console.WriteLine($"Mesh '{mesh.Id}' updated."))
.ContinueOnCapturedContext(false)
.StartAsync(exception => throw new HttpRequestException(exception.Message));
...
await meshFlowListener.StopAsync();
}Note:
MeshCreated&MeshUpdatedwill provide only the id of the mesh as a parameter, whileOnMeshCreated&OnMeshUpdatedwill provide the full mesh data.
You can control how the mesh data is encoded into JSON. Once you've implemented the IJsonSerializer interface, you can pass the implementaton to the CreateMeshFlow and CreateMeshStore methods.
class CustomSerializer : IJsonSerializer
{
public Task<HttpContent> SerializeAsync<T>(T data, CancellationToken cancellationToken)
{
...
}
public Task<T> DeserializeAsync<T>(HttpContent httpContent, CancellationToken cancellationToken)
{
...
}
}
static class Program
{
static async Task Main()
{
using var vertexFlow = new VertexFlow("https://localhost:5001");
var customSerializer = new CustomSerializer();
var meshFlow = vertexFlow.CreateMeshFlow<CustomMesh>(customSerializer);
var meshStore = vertexFlow.CreateMeshStore<CustomMesh>(customSerializer);
...
}
}You will find two custom json serializers in the
benchmarks/VertexFlow.SDK.Benchmark/JsonSerializersdirectory.
Make sure the src/VertexFlow.WebAPI project is running to be able to transfer mesh data.
You will find a complete example in the
src/VertexFlow.RevitAddinproject.
- Create a class to store mesh data
- Extract
Meshfrom anElement - Construct
Mesh Datafrom the extractedMesh - Send the
Mesh Data
Note:
MeshDataConstructormirrors geometry along theX-axisdue to theUnitycoordinate system. This approach avoids any transformation on theUnityside. But if you want to use this geometry in different 3D engines at the same time, it takes a trade-off to decide where to manually mirror it back.
Once you've configured your unity project:
-
Create a class to store mesh data
UnityMesh
using UnityEngine; using VertexFlow.Core.Models; public class UnityMesh : MeshData<Vector3> { }
-
Implement the
MeshCreatorclassMeshCreator
using UnityEngine; public class MeshCreator { private readonly Material _meshMaterial; private readonly Transform _meshContainer; public MeshCreator(Transform meshContainer, Material meshMaterial) { _meshMaterial = meshMaterial; _meshContainer = meshContainer; // Rotate due to Revit coordinate system. _meshContainer.rotation = Quaternion.Euler(-90, 0, 0); } public MeshFilter CreateMesh(UnityMesh meshData) { var gameObj = new GameObject(meshData.Id); // Sets mesh data to the game object. var meshFilter = gameObj.AddComponent<MeshFilter>(); SetMeshData(meshFilter.mesh, meshData); // Sets default material. gameObj.AddComponent<MeshRenderer>().sharedMaterial = _meshMaterial; // Sets parent to the game object. gameObj.transform.SetParent(_meshContainer, false); return meshFilter; } public void RebuildMesh(Mesh mesh, UnityMesh data) { mesh.Clear(); SetMeshData(mesh, data); } private void SetMeshData(Mesh mesh, UnityMesh data) { mesh.vertices = data.Vertices; mesh.triangles = data.Triangles; if (data.Normals.Length == data.Vertices.Length) { mesh.normals = data.Normals; } else { mesh.RecalculateNormals(); } mesh.Optimize(); } }
-
Implement the
UnityMeshProviderclassUnityMeshProvider
using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using VertexFlow.SDK.Extensions; using VertexFlow.SDK.Interfaces; using VertexFlow.SDK.Listeners; using VertexFlow.SDK.Listeners.Interfaces; public class UnityMeshProvider : IDisposable { private readonly MeshCreator _meshCreator; private readonly Dictionary<string, MeshFilter> _meshes; private readonly IMeshStore<UnityMesh> _meshStore; private readonly IMeshFlowListener _meshFlowListener; private readonly VertexFlow.SDK.VertexFlow _vertexFlow; public UnityMeshProvider(Transform meshContainer, Material defaultMaterial) { _meshes = new Dictionary<string, MeshFilter>(); _meshCreator = new MeshCreator(meshContainer, defaultMaterial); _vertexFlow = new VertexFlow.SDK.VertexFlow("https://localhost:5001"); _meshStore = _vertexFlow.CreateMeshStore<UnityMesh>(); _meshFlowListener = _vertexFlow .CreateMeshFlowListener() .WithStore(_meshStore) .OnMeshCreated(CreateMesh) .OnMeshUpdated(RebuildMesh); } public async Task StartMeshFlowListenerAsync() { await _meshFlowListener.StartAsync(); } public async Task LoadAllMeshesAsync() { foreach (var meshData in await _meshStore.GetAllAsync()) { CreateMesh(meshData); } } public async Task StopMeshFlowListenerAsync() { await _meshFlowListener.StopAsync(); } public void Dispose() { _meshFlowListener.Dispose(); _vertexFlow.Dispose(); } private void CreateMesh(UnityMesh meshData) { _meshes.Add(meshData.Id, _meshCreator.CreateMesh(meshData)); } private void RebuildMesh(UnityMesh meshData) { _meshCreator.RebuildMesh(_meshes[meshData.Id].mesh, meshData); } }
-
Use the
UnityMeshProviderclass as followingApp
using Extensions; using UnityEngine; public class App : MonoBehaviour { [SerializeField] private Material _meshMaterial; [SerializeField] private Transform _meshContainer; private UnityMeshProvider _unityMeshProvider; private void Awake() { _unityMeshProvider = new UnityMeshProvider(_meshContainer, _meshMaterial); } private void OnEnable() { _unityMeshProvider.StartMeshFlowListenerAsync().Forget(); } private void Start() { _unityMeshProvider.LoadAllMeshesAsync().Forget(); } private void OnDisable() { _unityMeshProvider.StopMeshFlowListenerAsync().Forget(); } private void OnDestroy() { _unityMeshProvider.Dispose(); } }
For Unigine, all steps are the same as for Unity, except the UnigineMesh and MeshCreator classes.
UnigineMesh
using Unigine;
using VertexFlow.Core.Models;
public class UnigineMesh : MeshData<vec3>
{
}MeshCreator
using Unigine;
namespace UnigineApp
{
public class MeshCreator
{
private readonly vec3 _transformVertex;
public MeshCreator()
{
_transformVertex = new vec3(-0.1f, 0.1f, 0.1f);
}
public ObjectMeshDynamic CreateMesh(UnigineMesh meshData)
{
var mesh = new ObjectMeshDynamic
{
MeshName = meshData.Id
};
mesh.SetMaterial("mesh_base", "*");
SetMeshData(mesh, meshData);
return mesh;
}
public void RebuildMesh(ObjectMeshDynamic mesh, UnigineMesh data)
{
mesh.ClearVertex();
mesh.ClearIndices();
mesh.ClearSurfaces();
SetMeshData(mesh, data);
mesh.FlushVertex();
mesh.FlushIndices();
}
private void SetMeshData(ObjectMeshDynamic mesh, UnigineMesh data)
{
// Allocate space in index and vertex buffers.
mesh.AllocateIndices(data.Triangles.Length);
mesh.AllocateVertex(data.Vertices.Length);
// Add vertices.
for (var i = 0; i < data.Vertices.Length; i++)
{
mesh.AddVertex(data.Vertices[i] * _transformVertex);
}
// Add indices for created vertices.
for (var i = data.Triangles.Length - 1; i >= 0; i--)
{
mesh.AddIndex(data.Triangles[i]);
}
// Calculate tangent vectors.
mesh.UpdateTangents();
// Optimize vertex and index buffers, if necessary.
mesh.UpdateIndices();
// Calculate a mesh bounding box.
mesh.UpdateBounds();
}
}
}Note: Unlike Unity, when the Unigine app's Main method is invoked, SynchronizationContext.Current will return null. That means that when you invoke an asynchronous method, it will not return to the main thread, but the
Unigine APIhas to run from the main thread. You will find more information here.
You will find two custom json serializers in the
benchmarks/VertexFlow.SDK.Benchmark/JsonSerializersdirectory.
You can optimize performance and memory usage by writing a custom json serializer.
You will find all benchmarks in the
benchmarks/VertexFlow.SDK.Benchmarkproject.
The benchmarks were run on the dataset with realistic mesh data. The tests compare the JsonSerializer in the Newtonsoft.Json namespace (used by default) with two custom serializers based on JsonSerializer in the System.Text.Json namespace.
Environment
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19041.1165 (2004/May2020Update/20H1) Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET SDK=5.0.301 [Host] : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
| Method | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | | ------------------------------- |---------:|---------:|---------:|------:|----------:|----------:|----------:|----------:| | Newtonsoft_Stream | 150.1 ms | 2.88 ms | 2.41 ms | 1.00 | 3000.0000 | 1000.0000 | - | 24 MB | | SystemTextJson_Stream | 114.2 ms | 2.02 ms | 1.89 ms | 0.76 | 400.0000 | 200.0000 | - | 4 MB | | SystemTextJson_RecyclableStream | 113.9 ms | 1.61 ms | 1.51 ms | 0.76 | 400.0000 | 200.0000 | - | 4 MB |
| Method | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | |-------------------------------- |---------:|---------:|---------:|------:|----------:|----------:|----------:|----------:| | Newtonsoft_Stream | 933.5 ms | 7.35 ms | 6.88 ms | 1.00 | 2000.0000 | 1000.0000 | 1000.0000 | 17 MB | | SystemTextJson_Stream | 934.8 ms | 11.53 ms | 10.22 ms | 1.00 | 1000.0000 | 1000.0000 | 1000.0000 | 7 MB | | SystemTextJson_RecyclableStream | 931.6 ms | 6.41 ms | 5.68 ms | 1.00 | - | - | - | 1 MB |
Note: Make sure your database contains data before running the
VertexFlow.SDK.Benchmarkproject.
Usage is provided under the MIT License.

