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.addin
file and replace theAssembly
section with your output directory - Open the
VertexFlow.RevitAddin.csproj
file and replace theDestinationFolder
in the targetAfterBuild
section
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.RevitAddin
project from the solution.
-
Create
Assembly Definition
in theAssets/Plugins/VertexFlow
folder withVertexFlow
name -
Copy following libraries to the
Assets/Plugins/VertexFlow/libs
directory- VertexFlow.Core.dll
- VertexFlow.SDK.dll
- VertexFlow.SDK.Extensions.dll
- VertexFlow.SDK.Listeners.dll
-
Create
signalr.ps1
file in theAssets/Plugins/VertexFlow/libs
foldersignalr.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/libs
directory to import the target .dll files./signalr.ps1
You will find a complete example in the
samples/VertexFlow.SDK.Sample
project.
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
Vector3
fromVertexFlow.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
&MeshUpdated
will provide only the id of the mesh as a parameter, whileOnMeshCreated
&OnMeshUpdated
will 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/JsonSerializers
directory.
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.RevitAddin
project.
- Create a class to store mesh data
- Extract
Mesh
from anElement
- Construct
Mesh Data
from the extractedMesh
- Send the
Mesh Data
Note:
MeshDataConstructor
mirrors geometry along theX-axis
due to theUnity
coordinate system. This approach avoids any transformation on theUnity
side. 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
MeshCreator
classMeshCreator
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
UnityMeshProvider
classUnityMeshProvider
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
UnityMeshProvider
class 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 API
has 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/JsonSerializers
directory.
You can optimize performance and memory usage by writing a custom json serializer.
You will find all benchmarks in the
benchmarks/VertexFlow.SDK.Benchmark
project.
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.Benchmark
project.
Usage is provided under the MIT License.