back to index

from bim to unity: real-time importer

Problem: BIM models change constantly during construction. Pre-baked Unity builds are outdated before deployment. Users need to see current model state: latest revisions, updated elements, recent changes. The standard approach (export from Revit, import to Unity, build, deploy) requires a full rebuild cycle for every model update. Slow. Brittle. Doesn't scale.

Solution: Dynamic loading. Connect directly to the BIM database, stream geometry and metadata at runtime, display current state without rebuilds. A type library pattern keeps memory efficient by loading unique element types once and instantiating many times.

Architecture Strategy

The importer operates in three phases. First, type extraction: load unique element types once and create reusable prefabs. Second, element instantiation: place instances of those types at their specific locations. Third, hierarchy construction: organize everything by category and storey for filtering.

This separation matters because it mirrors how BIM data is actually structured. Types define what something is (geometry, materials). Elements define where instances of that type exist (position, rotation, parameters). Keeping these concerns separate makes the import pipeline predictable and the runtime memory efficient.

This separates concerns cleanly. Types handle geometry. Elements handle positioning. Hierarchy handles organization.

Type Library Pattern

BIM models have thousands of elements but relatively few unique types. A building might have 500 doors, but only 15 door types. Load each type once, instantiate many times. This insight drives the entire architecture.

public static ModelController ImportBimJsonModel(
    Transform modelParent,
    ImportReport report,
    IList<string> typeStrings,
    IList<string> elementStrings)
{
    Dictionary<long, BimObject> typelib = new Dictionary<long, BimObject>();

    report.StartTask("TypeImport", "importing all types", ImportReport.MsgType.Info);

    List<(long id, string name)> settingsStoreys = 
        ApplicationConfiguration.Configuration.Model.Storeys;

    for (int i = 0; i < settingsStoreys.Count; i++)
    {
        report.StartTask($"StoreyTyped: {settingsStoreys[i]}", 
            $"storey {settingsStoreys[i]} types downloaded", 
            ImportReport.MsgType.Debug);

        // Add types to typelib, duplicates skipped
        BimDataConverter.GetTypesFrom<TypeProxy>(typeStrings[i], typelib);

        report.EndTask($"StoreyTyped: {settingsStoreys[i]}", "");
    }

    report.EndTask("TypeImport", "");

    report.StartTask("ElementImport", "importing all elements", 
        ImportReport.MsgType.Info);

    for (int i = 0; i < settingsStoreys.Count; i++)
    {
        report.StartTask($"StoreyElem:{settingsStoreys[i]}", 
            $"storey {settingsStoreys[i]} elements downloaded", 
            ImportReport.MsgType.Debug);
        
        // Add element instances for types we've just created
        BimDataConverter.AddElements(elementStrings[i], 
            settingsStoreys[i].id, settingsStoreys[i].name, typelib);
        
        report.EndTask($"StoreyElem:{settingsStoreys[i]}", "elements created");
    }

    report.EndTask("ElementImport", "");

    return CreateModel(typelib, modelParent);
}

The import happens in two passes. First, process all type definitions and build the type library. Second, process element instances, referencing types by ID. This means geometry data (vertices, submeshes, materials) lives in the type library, while elements store only transform and parameter data. The memory savings are significant: instead of duplicating geometry for every door, we store it once and instantiate pointers.

Coordinate System Transformation

BIM models use right-handed coordinate systems. Unity uses left-handed. Import geometry directly and everything appears mirrored. Walls face the wrong way. Doors open backward.

The naive fix would transform every vertex during import. Expensive and error-prone. Instead, we use a handedness corrector: a parent transform that flips the entire hierarchy in one operation.

private static ModelController CreateModel(
    Dictionary<long, BimObject> data, 
    Transform parent)
{
    Transform handednessCorrector = new GameObject("Handedness Corrector").transform;

    handednessCorrector.transform.SetParent(parent);
    handednessCorrector.transform.localPosition = Vector3.zero;
    handednessCorrector.transform.localRotation = Quaternion.identity;
    handednessCorrector.transform.localScale = Vector3.one;
    handednessCorrector.gameObject.tag = "StoreyParent";

    // ... build hierarchy under handednessCorrector ...

    if (!ApplicationConfiguration.Configuration.Model.UseRhs)
    {
        handednessCorrector.localScale = new Vector3(-1, 1, 1);
    }

    ModelController mc = parent.gameObject.AddComponent<ModelController>();
    mc.Categories = handednessCorrector.gameObject
        .AddComponent<CategoryController>();

    return mc;
}

All geometry imports normally. After construction, if the source uses right-handed coordinates, we flip the entire hierarchy by setting X scale to -1. This mirrors everything across the YZ plane. All child transforms flip correctly. Rotations remain valid. One operation instead of transforming each vertex.

The Origin Problem: Floating-Point Precision

BIM models use real-world coordinates. A building in Amsterdam might sit at coordinates like (121000, 485000) meters in the Dutch RD coordinate system. Import those values directly into Unity and everything breaks.

Unity uses 32-bit floats for position values. Single-precision floating-point has roughly 7 significant digits of precision. At position (121000, 0, 485000), that precision translates to errors measured in centimeters. Move the camera, and objects jitter. Render a wall, and vertices dance. This isn't a Unity bug. It's fundamental to how floating-point math works. The further from origin, the larger the precision errors.

The symptoms are obvious once you know what to look for: visual jittering during camera movement, Z-fighting between surfaces that should be distinct, physics instabilities, mesh flickering during transforms. A building 500km from origin exhibits millimeter-scale jitter. Unacceptable for construction AR where we need sub-centimeter accuracy.

The solution is to place everything at origin. Offset all coordinates relative to a reference point within the model itself.

// BIM coordinates: real-world survey position
Vector3 bimPosition = element.transform.ExtractPosition();  // e.g. (121543, 12, 485221)

// Reference point: center of the model's bounding box or first element
Vector3 modelOrigin = modelReferencePoint;  // e.g. (121500, 0, 485200)

// Unity position: relative to model origin, now near (0,0,0)
Vector3 unityPosition = (bimPosition - modelOrigin) * scale;  // e.g. (43, 12, 21) * 0.001

All elements position relative to the model's own origin. The model itself sits at Unity's world origin. Every position value stays small. Precision stays high.

The AR anchoring system handles real-world positioning separately. QR markers encode their real-world locations. When a marker is tracked, the entire model transforms correctly, but the internal geometry stays origin-relative, preserving precision. This is why the ModelController parents to a transform hierarchy rather than positioning elements in absolute world space. The parent transform handles world positioning. Child elements stay local.

Result: Sub-millimeter precision regardless of where the building actually sits on Earth.

Hierarchical Organization

BIM models have natural hierarchy: Category → Storey → Element. A wall on the ground floor belongs to the Walls category and the Ground Floor storey. The importer maintains this structure for filtering and visibility control, which matters enormously in the field. A site engineer checking structural columns doesn't want to see every door and window. A foreman on the third floor doesn't need the basement cluttering the view.

Dictionary<string, Transform> storeys = new Dictionary<string, Transform>();
Dictionary<int, Transform> categories = new Dictionary<int, Transform>(23);

foreach (KeyValuePair<long, BimObject> type in data)
{
    int cat = (int)type.Value.category;
    Transform catTransform;
    
    if (categories.ContainsKey(cat))
    {
        catTransform = categories[cat];
    }
    else
    {
        catTransform = new GameObject(((ModelImporter.Category)cat)
            .ToString()).transform;
        catTransform.SetParent(handednessCorrector);
        categories.Add(cat, catTransform);
    }

    foreach (KeyValuePair<Storey, List<Element>> storey in type.Value.elements)
    {
        Transform storeyTransform;
        string key = $"{cat}-{storey.Key.id}";

        if (storeys.ContainsKey(key))
        {
            storeyTransform = storeys[key];
        }
        else
        {
            storeyTransform = new GameObject(storey.Key.name).transform;
            storeyTransform.SetParent(catTransform);
            storeys.Add(key, storeyTransform);
        }

        foreach (Element element in storey.Value)
        {
            Transform elem;
            if (prefabPlaced)
            {
                elem = Instantiate(type.Value.prefab).transform;
            }
            else
            {
                type.Value.prefab.SetActive(true);
                elem = type.Value.prefab.transform;
                prefabPlaced = true;
            }

            // Add element parameters
            elem.GetComponent<BimMetaData>()
                .AddData(element.parameters, element.guid, element.id);

            elem.SetParent(storeyTransform);
            elem.position = element.transform.ExtractPosition() * 
                ApplicationConfiguration.Configuration.Model.Scale;
            elem.rotation = element.transform.ExtractRotation();
            elem.localScale = element.transform.ExtractScale();
        }
    }
}

This creates hierarchy:

Handedness Corrector
  ├─ Walls
     ├─ Ground Floor
        ├─ Wall_001
        └─ Wall_002
     └─ First Floor
         └─ Wall_003
  ├─ Doors
     ├─ Ground Floor
        └─ Door_001
     └─ First Floor
         └─ Door_002
  └─ Structure
      └─ Ground Floor
          └─ Column_001

This structure enables powerful filtering without custom logic. Categories let users hide all doors and show only structure. Storeys let them isolate a specific floor. Both use Unity's built-in transform hierarchy. To hide all doors, just deactivate the Doors GameObject. To show only the ground floor, deactivate every other storey. Simple, fast, predictable.

Prefab Reuse Strategy

A small optimization that adds up. The first element of each type keeps the original prefab. Subsequent elements instantiate copies. This avoids one instantiation per type.

bool prefabPlaced = false;

foreach (Element element in storey.Value)
{
    Transform elem;
    if (prefabPlaced)
    {
        elem = Instantiate(type.Value.prefab).transform;
    }
    else
    {
        type.Value.prefab.SetActive(true);
        elem = type.Value.prefab.transform;
        prefabPlaced = true;
    }
    
    // Position and configure element...
}

The first instance reuses the prefab created during type loading. For models with thousands of types, skipping one instantiation per type accumulates to meaningful savings during import.

JSON Proxy Pattern

BIM data is verbose. A single element can have dozens of properties: material specs, manufacturer info, installation dates, cost data, revision history. Most of that isn't needed for rendering. Deserializing everything would waste time and memory.

public static void GetTypesFrom<T>(string data, 
    Dictionary<long, BimObject> typelib) 
    where T : TypeProxyBase
{
    // Create type proxy collection extracting minimally used data
    ProxyCollection<T> types = ProxyCollection<T>.Get(data);
    
    foreach (T typeProxy in types.proxies)
    {
        if (typelib.ContainsKey(typeProxy.id))
        {
            Debug.LogError("Double types");
            continue;
        }

        typelib.Add(typeProxy.id, new BimObject(typeProxy));
    }
}

The proxy pattern solves this. We define lightweight proxy classes that extract only what rendering needs: ID, GUID, vertices, submeshes, category, name, and essential parameters. Everything else is ignored during import. If detailed data is needed later, request it specifically for selected elements rather than loading everything upfront. A user taps a wall in AR, we fetch that wall's full property set on demand.

Performance Characteristics

On typical hardware, the importer processes around 2000 elements per second. A medium-sized building with 50,000 elements uses about 200MB of memory. Hierarchy construction completes in under 500ms after data arrives.

The type library pattern is what makes this possible. Without it, memory scales linearly with element count. With it, memory scales with unique type count, typically 5-10% of total elements. That's a 10-20x reduction in geometry storage. A building with 50,000 elements but only 3,000 unique types stores geometry for 3,000 objects, not 50,000.

Scale and Transform Extraction

BIM stores transforms as 4x4 matrices. Unity uses position, rotation, and scale as separate components. The matrix extensions decompose each element's transform into these three parts. Position gets scaled to match Unity's world units (BIM typically uses meters, but the scale factor is configurable). Rotation extracts as a quaternion. Scale separates from other components.

elem.position = element.transform.ExtractPosition() * 
    ApplicationConfiguration.Configuration.Model.Scale;
elem.rotation = element.transform.ExtractRotation();
elem.localScale = element.transform.ExtractScale();

This preserves all transform information from the source while adapting to Unity's conventions. Elements maintain correct relative positions and orientations.

Metadata Attachment

Each element carries metadata from the BIM model: parameters, properties, relationships. The BimMetaData component attaches this data to the GameObject.

elem.GetComponent<BimMetaData>()
    .AddData(element.parameters, element.guid, element.id);

This enables selection and inspection in AR. User taps an element, we retrieve its BIM properties instantly. Material specifications, manufacturer information, installation dates, cost data, maintenance schedules, custom fields. All available for display or filtering without additional database queries. The metadata stays attached to the GameObject, accessible throughout the application.

Why Dynamic Import Matters

Pre-baked models lock in specific model state. Any change requires a full rebuild and redeployment cycle. This doesn't match construction reality where models update daily.

Dynamic import changes what's possible. Launch the app, load the latest model state. No rebuild cycle: model updates don't require app updates. Selective loading: pull only relevant storeys or categories. A/B comparison: load two model versions simultaneously to see what changed. Parametric exploration: adjust parameters, reload, see results.

The type library pattern makes this performant. The proxy pattern keeps memory reasonable. The hierarchical organization maintains usability.

Most AR-BIM solutions require pre-baked models. This approach enables dynamic, on-demand loading from live BIM databases. The model in AR is always the model in the database. No stale data. No rebuild delays. That matters on a construction site where decisions depend on current information.