mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
docs: rewrite viewport architecture page + add .ifcview format reference
Rewrite the BonsaiViewer viewport architecture page to match the current renderer (updated type names, streaming/sidecar flow). Add a dedicated .ifcview sidecar format reference page and link it from the ifcopenshell formats toctree, and polish the Bonsai intro copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
Bonsai
|
||||
======
|
||||
|
||||
Bonsai lets you analyse, create, and modify OpenBIM with Blender. For more
|
||||
information, visit the `Bonsai website <https://bonsaibim.org>`_.
|
||||
Bonsai is a native IFC authoring platform to analyse, create, and modify
|
||||
OpenBIM with Blender.
|
||||
|
||||
For more information, visit the `Bonsai website <https://bonsaibim.org>`_.
|
||||
|
||||
@@ -101,6 +101,11 @@ without repeating tessellation, packing, quantisation, and instance rebuilds.
|
||||
It is not a general interchange format or a full semantic storage backend. For
|
||||
model data, pair it with the original IFC file or with RocksDB.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
ifcview_format
|
||||
|
||||
RDBVIEW
|
||||
-------
|
||||
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
IfcView format
|
||||
==============
|
||||
|
||||
``.ifcview`` is a lossy binary geometry cache used by the Bonsai Viewer and
|
||||
IfcViewer. It is a geometry cache, not an IFC exchange format. It stores
|
||||
quantised, compressed, spatially sorted streamable chunks of tessellated
|
||||
geometry, instancing, LODs, georeferencing, and basic element identifiers.
|
||||
|
||||
The format is optimised for three requirements:
|
||||
|
||||
- fast load and reuse of model geometry for analysis or visualisation
|
||||
- direct upload to a viewer's GPU-facing mesh and instance layout
|
||||
- compressed chunked streaming, especially for web loading by byte range
|
||||
|
||||
It intentionally does not store non-geometric IFC data or original IFC
|
||||
parametric geometry. It is recommended to be combined with the original
|
||||
``.ifc`` file, a ``.ifcdb``/``.rdb`` model store, or an ``.rdbview`` package
|
||||
when additional data is needed.
|
||||
|
||||
The format writes native C++ structs directly for several tables. This makes it
|
||||
compact and cheap to load, but it also means that ``.ifcview`` should not be
|
||||
treated as a stable, language-neutral interchange specification.
|
||||
|
||||
The source of truth for the format is the C++ sidecar implementation in
|
||||
``src/ifcviewer``. The current on-disk version is version 17.
|
||||
|
||||
Top-level layout
|
||||
----------------
|
||||
|
||||
.. csv-table::
|
||||
:header: "Section", "Description"
|
||||
|
||||
"SidecarHeader", "Versioning and format metadata"
|
||||
"uint64 geometry_section_size", "Number of bytes from start of geometry section to byte immediately before the geometry metadata block. May be used by streaming readers to skip bulk geometry and first fetch metadata."
|
||||
"Geometry section", "Streamable compressed chunks of geometry"
|
||||
"Geometry metadata block", "Mesh records, instance records, georeferencing data, and chunk frame offsets"
|
||||
"Element metadata block", "Element IDs, GUIDs, names, and IFC classes"
|
||||
|
||||
Both metadata blocks use the same compressed block wrapper:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
uint64 compressed_size
|
||||
uint64 raw_size
|
||||
byte[compressed_size] zstd_frame
|
||||
|
||||
The geometry section is also compressed with zstd, but it is not one large
|
||||
frame. Each streaming chunk has its own vertex frame and index frame so a
|
||||
loader can fetch and decompress only the chunks it needs.
|
||||
|
||||
For web streaming, the loader would read:
|
||||
|
||||
1. the 20-byte head, consisting of the 12-byte header and 8-byte geometry size
|
||||
2. the geometry metadata block header and compressed payload
|
||||
3. the element metadata block header, so it can remember where the UI metadata
|
||||
lives
|
||||
|
||||
After that, visible chunks are loaded asynchronously by byte range. The
|
||||
element metadata payload is fetched only when UI code asks for element
|
||||
metadata.
|
||||
|
||||
|
||||
Sidecar header
|
||||
--------------
|
||||
|
||||
``SidecarHeader`` is 12 bytes:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
struct SidecarHeader {
|
||||
uint32 magic;
|
||||
uint32 version;
|
||||
uint32 endian;
|
||||
};
|
||||
|
||||
The current values are:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Field
|
||||
- Value
|
||||
- Purpose
|
||||
* - Magic
|
||||
- ``0x49465657``
|
||||
- Identifies an ``IFVW`` sidecar.
|
||||
* - Version
|
||||
- ``17``
|
||||
- Selects the current layout version. Versions may not be compatible.
|
||||
* - Endian marker
|
||||
- ``0x01020304``
|
||||
- Rejects files written with a different byte order.
|
||||
|
||||
If any of these values do not match, the ifcview cache must be rejected.
|
||||
|
||||
Geometry section
|
||||
----------------
|
||||
|
||||
The geometry section is the streamable part of the file. It contains, for each
|
||||
chunk, two zstd frames:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
chunk 0 vertex frame
|
||||
chunk 0 index frame
|
||||
chunk 1 vertex frame
|
||||
chunk 1 index frame
|
||||
...
|
||||
|
||||
The metadata does not discover these frames by scanning. Instead, the geometry
|
||||
metadata block stores the ``SidecarChunk`` table that locates each chunk's
|
||||
compressed vertex and index frames.
|
||||
|
||||
Inside a decompressed chunk, data is chunk-local:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
vertices for mesh first_mesh
|
||||
vertices for mesh first_mesh + 1
|
||||
...
|
||||
LOD0 indices for mesh first_mesh
|
||||
LOD0 indices for mesh first_mesh + 1
|
||||
...
|
||||
LOD1 indices for mesh first_mesh
|
||||
LOD1 indices for mesh first_mesh + 1
|
||||
...
|
||||
|
||||
This order matches the renderer's streamed chunk upload path. Vertex offsets
|
||||
and index offsets in ``MeshInfo`` describe the whole-model logical layout, but
|
||||
the chunk upload path computes chunk-local offsets when it builds runtime chunk
|
||||
state.
|
||||
|
||||
Vertex format
|
||||
-------------
|
||||
|
||||
Each stored vertex is 12 bytes:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Byte offset
|
||||
- Type
|
||||
- Meaning
|
||||
* - ``0``
|
||||
- ``uint16[3]``
|
||||
- Quantised local position.
|
||||
* - ``6``
|
||||
- ``int8[2]``
|
||||
- Octahedral-encoded normal.
|
||||
* - ``8``
|
||||
- ``uint8[4]``
|
||||
- RGBA colour.
|
||||
|
||||
The position is quantised against the mesh's local axis-aligned bounding box,
|
||||
stored in ``MeshInfo.local_aabb_min`` and ``MeshInfo.local_aabb_max``. This
|
||||
bounding box is the quantisation basis. In other words, the format stores each
|
||||
coordinate as a normalised integer within the mesh-local min/max range, rather
|
||||
than storing the original float coordinate.
|
||||
|
||||
Conceptually, encoding does this per axis:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
t = (position - local_aabb_min) / (local_aabb_max - local_aabb_min)
|
||||
stored = round(clamp(t, 0, 1) * 65535)
|
||||
|
||||
Decoding does the inverse:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
t = stored / 65535
|
||||
position = mix(local_aabb_min, local_aabb_max, t)
|
||||
|
||||
The implementation stores ``extent_recip`` while encoding, which is simply
|
||||
``1 / (local_aabb_max - local_aabb_min)`` for each axis. Degenerate axes use
|
||||
``0`` so all coordinates on that axis quantise to the same value.
|
||||
|
||||
Normals use two signed bytes with octahedral encoding. This is less precise
|
||||
than storing three floats, but is small and adequate for typical BIM geometry,
|
||||
which is dominated by planar and axis-aligned surfaces. Colour is stored as the
|
||||
four bytes used by the viewer's packed RGBA path.
|
||||
|
||||
Geometry metadata block
|
||||
-----------------------
|
||||
|
||||
The geometry metadata block is the minimum metadata required to create the
|
||||
runtime model and begin painting geometry. Its raw, decompressed order is:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
vector<MeshInfo> meshes
|
||||
vector<InstanceInfo> instances
|
||||
uint32 has_coordinate_operation
|
||||
double[16] coordinate_operation_meters
|
||||
double project_length_to_meters
|
||||
double map_unit_to_meters
|
||||
vector<SidecarChunk> chunks
|
||||
|
||||
``MeshInfo`` is 56 bytes and describes one reusable mesh:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Field
|
||||
- Meaning
|
||||
* - ``vbo_byte_offset``
|
||||
- Byte offset of the mesh's vertices in the logical whole-model vertex buffer.
|
||||
* - ``vertex_count``
|
||||
- Number of 12-byte vertices.
|
||||
* - ``ebo_byte_offset``
|
||||
- Byte offset of the mesh's LOD0 indices in the logical index buffer.
|
||||
* - ``index_count``
|
||||
- Number of LOD0 ``uint32`` indices.
|
||||
* - ``local_aabb_min`` / ``local_aabb_max``
|
||||
- Mesh-local bounds and the quantisation basis for vertex positions.
|
||||
* - ``first_instance`` / ``instance_count``
|
||||
- Range of instances that reference this mesh after sidecar layout.
|
||||
* - ``lod1_ebo_byte_offset`` / ``lod1_index_count``
|
||||
- Optional decimated index range for LOD1. A count of ``0`` means LOD1 is unavailable.
|
||||
|
||||
``InstanceInfo`` is 232 bytes and records one placed occurrence of a mesh. The
|
||||
important fields are:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Field
|
||||
- Meaning
|
||||
* - ``mesh_id``
|
||||
- Index into the mesh table.
|
||||
* - ``object_id``
|
||||
- Viewer object identifier used for selection and lookup.
|
||||
* - ``color_override_rgba8``
|
||||
- Optional per-instance colour override. ``0`` means use the baked vertex colour.
|
||||
* - ``model_id``
|
||||
- Source model identifier within the viewer.
|
||||
* - ``placement_transformation``
|
||||
- Double-precision placement emitted by the geometry streamer, before final federation and false-origin composition.
|
||||
* - ``transform``
|
||||
- Float render transform for the default stage state. Loaders may recompute it from ``placement_transformation`` and current stage matrices.
|
||||
* - ``world_aabb_min`` / ``world_aabb_max``
|
||||
- World-space instance bounds used for chunk bounds, culling, and view fitting.
|
||||
|
||||
Both transform forms are stored for precision and reuse. The double placement
|
||||
keeps large IFC coordinates intact until the viewer has applied coordinate
|
||||
operation, model transformation, and false-origin matrices. The float transform
|
||||
is the GPU-facing result for the default composition.
|
||||
|
||||
The georeferencing fields cache enough of the model's coordinate operation and
|
||||
unit scale to load a sidecar without reparsing the IFC source solely to recover
|
||||
map conversion state. If the source map conversion changes, the sidecar must be
|
||||
deleted and rebuilt.
|
||||
|
||||
``SidecarChunk`` is 56 bytes and records one streamable range of meshes. Each
|
||||
chunk maps to two compressed geometry frames in the geometry section:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Field
|
||||
- Meaning
|
||||
* - ``first_mesh``
|
||||
- First mesh index covered by the chunk.
|
||||
* - ``mesh_count``
|
||||
- Number of consecutive meshes covered by the chunk.
|
||||
* - ``v_comp_off`` / ``v_comp_size``
|
||||
- Byte offset and compressed byte size of the chunk's vertex frame, relative to the start of the geometry section.
|
||||
* - ``v_raw_size``
|
||||
- Decompressed byte size of the chunk's vertex data.
|
||||
* - ``i_comp_off`` / ``i_comp_size``
|
||||
- Byte offset and compressed byte size of the chunk's index frame, relative to the start of the geometry section.
|
||||
* - ``i_raw_size``
|
||||
- Decompressed byte size of the chunk's index data.
|
||||
|
||||
The chunk table is part of the geometry metadata because the viewer needs it
|
||||
before it can request geometry. On the web path, the loader reads the header
|
||||
and geometry metadata block, creates a model with non-resident chunks, and then
|
||||
starts fetching visible chunk frames by byte range.
|
||||
|
||||
Element metadata block
|
||||
-----------------------
|
||||
|
||||
The element metadata block stores non-geometric element lookup data used by UI
|
||||
features such as picking, tree display, object labels, and search.
|
||||
|
||||
Its raw, decompressed order is:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
vector<ElementTableRecord> elements
|
||||
uint32 string_table_bytes
|
||||
char[string_table_bytes] string_table
|
||||
|
||||
Each ``ElementTableRecord`` is a fixed-size 36-byte record:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
struct ElementTableRecord {
|
||||
uint32 object_id;
|
||||
uint32 model_id;
|
||||
int32 ifc_id;
|
||||
uint32 guid_offset;
|
||||
uint32 guid_length;
|
||||
uint32 name_offset;
|
||||
uint32 name_length;
|
||||
uint32 type_offset;
|
||||
uint32 type_length;
|
||||
};
|
||||
|
||||
Strings are stored once in the string table and referenced by offset and
|
||||
length.
|
||||
|
||||
Splitting this block from geometry metadata is important for first paint. A
|
||||
large model can have substantial names, GlobalIds, and type strings. The web
|
||||
viewer can show geometry after the geometry metadata block is available, then
|
||||
fetch this metadata later when the UI needs it.
|
||||
|
||||
Chunk planning
|
||||
--------------
|
||||
|
||||
Chunks are spatial groups of meshes. During sidecar creation, the builder:
|
||||
|
||||
1. Computes a centroid for each mesh from the average of its instance
|
||||
world-AABB centres.
|
||||
2. Sorts mesh ids by a 3D Morton code, also known as Z-order.
|
||||
3. Greedily packs the sorted meshes into 4 MiB vertex-byte chunks. A single
|
||||
mesh larger than that limit is kept whole in an oversized chunk. Meshes are
|
||||
not split.
|
||||
4. Reorders meshes, vertices, indices, and instances so each chunk is a
|
||||
consecutive mesh range.
|
||||
5. Writes these offsets into the ``SidecarChunk`` table so that loaders can
|
||||
directly jump to the compressed chunk as needed.
|
||||
|
||||
How the file is produced
|
||||
------------------------
|
||||
|
||||
When no usable sidecar is available, the viewer falls back to the geometry
|
||||
streamer. The streamer emits:
|
||||
|
||||
- ``StreamedMesh`` once for each unique representation mesh
|
||||
- ``StreamedInstance`` for every placed occurrence of a mesh
|
||||
- ``ElementInfo`` records for viewer metadata
|
||||
|
||||
The sidecar builder consumes those streams. Mesh chunks are converted from the
|
||||
streamer transfer layout, which is seven floats per vertex
|
||||
(``position.xyz``, ``normal.xyz``, packed colour), into the 12-byte quantised
|
||||
vertex layout. Instance chunks become ``InstanceInfo`` records. Element info
|
||||
records become ``ElementTableRecord`` plus string table entries.
|
||||
|
||||
At finalisation, the builder adds LOD1 index buffers where useful, caches
|
||||
georeferencing state, lays out meshes in streaming chunk order, builds the chunk
|
||||
table, and writes the file.
|
||||
|
||||
Binary conventions
|
||||
------------------
|
||||
|
||||
The current writer uses these conventions:
|
||||
|
||||
- Multi-byte scalar values are written in native byte order and validated by
|
||||
the endian marker.
|
||||
- Counted vectors are written as ``uint32 count`` followed by
|
||||
``count * sizeof(T)`` bytes of raw table entries.
|
||||
- Metadata blocks are zstd-compressed as complete raw metadata buffers.
|
||||
- Geometry chunks store vertex bytes and index bytes as separate zstd frames.
|
||||
- Offsets stored in the chunk table are relative to the start of the geometry
|
||||
section, not relative to the start of the file.
|
||||
Reference in New Issue
Block a user