Geometry processing

Geometry is specified in many ways in IFC. Some geometry is defined explicitly with coordinates, vertices, and faces. Some geometry is defined implicitly with equations, boolean operations, and parametric shapes.

Individual processing

The simplest way to process any geometry in a standardised fashion is to ask a converter for the BRep of a product, and to triangulate that. It provides a list of vertices, edges, and faces, or alternatively the untriangulated BRep of the geometry kernel.

Warning

This section describes individual processing only. This is useful for learning how geometry processing works, but is not recommended for practical applications. See the Geometry iterator section below after reading this to see how to process geometry with multiple threads.

Here is a complete example of processing a single wall, and of everything that comes with it:

    // The equivalent of ifcopenshell.geom.create_shape() is to ask a converter
    // for the BRep of the product, and to triangulate that. Choosing a geometry
    // kernel has a big impact on speed and capability, the hybrid of the CGAL
    // simple kernel with OpenCASCADE as a fallback is recommended.
    ifcopenshell::geom::settings settings;
    ifcopenshell::geom::converter converter(
        ifcopenshell::geom::kernels::construct(&model, "hybrid-cgal-simple-opencascade", settings),
        &model, settings);

    // When an entire element is processed, its 3D representation is used, with
    // all of its openings applied.
    auto representation = converter.mapping()->representation_of(element);
    ifcopenshell::geom::native_element* brep = converter.create_brep_for_representation_and_product(representation, element);
    if (brep == nullptr) {
        std::cerr << "Shape creation failed" << std::endl;
        return 1;
    }
    ifcopenshell::geom::triangulation_element shape(*brep);
    delete brep;

    // The GUID and the ID of the element we processed.
    std::cout << shape.guid() << std::endl;
    std::cout << shape.id() << std::endl;
    // The element itself is one lookup away.
    model.instance_by_guid(shape.guid()).to_string(std::cout);
    std::cout << std::endl;

    // A unique geometry id, useful to check whether two geometries are
    // identical for caching and reuse. The naming scheme is:
    // IfcShapeRepresentation.id{-layerset-LayerSet.id}{-material-Material.id}{-openings-[Opening n.id ...]}{-world-coords}
    std::cout << shape.geometry().id() << std::endl;

    // A 4x4 matrix with the location and rotation of the element, in the form:
    // [ [ x_x, y_x, z_x, x   ]
    //   [ x_y, y_y, z_y, y   ]
    //   [ x_z, y_z, z_z, z   ]
    //   [ 0.0, 0.0, 0.0, 1.0 ] ]
    // The position is the last column, the rotation is described by the first
    // three columns, by explicitly specifying the local X, Y and Z axes. The
    // axes follow a right-handed coordinate system, and objects are never
    // scaled, so the scale factor of the matrix is always 1.
    const auto matrix = shape.transformation().data();
    const auto origin = matrix->translation_part();
    std::cout << origin.x() << ", " << origin.y() << ", " << origin.z() << std::endl;

    const auto& geometry = shape.geometry();
    // X Y Z of the vertices in a flattened list, e.g. [v1x, v1y, v1z, v2x, ...]
    // The vertices are local, relative to the transformation matrix above.
    const auto& verts = geometry.verts();
    // Indices of the vertices per edge, e.g. [e1v1, e1v2, e2v1, e2v2, ...]. These
    // are the original edges of the geometry, which may be quads or ngons.
    const auto& edges = geometry.edges();
    // Indices of the vertices per triangle face, e.g. [f1v1, f1v2, f1v3, ...].
    // Faces are always triangles.
    const auto& faces = geometry.faces();
    std::cout << verts.size() / 3 << " vertices, " << edges.size() / 2 << " edges, "
              << faces.size() / 3 << " faces" << std::endl;

    // Since the lists are flattened, you may prefer to group them.
    std::vector<std::array<double, 3>> grouped_verts;
    for (std::size_t i = 0; i + 2 < verts.size(); i += 3) {
        grouped_verts.push_back({verts[i], verts[i + 1], verts[i + 2]});
    }
    std::vector<std::array<int, 3>> grouped_faces;
    for (std::size_t i = 0; i + 2 < faces.size(); i += 3) {
        grouped_faces.push_back({faces[i], faces[i + 1], faces[i + 2]});
    }
    std::cout << grouped_verts.size() << " grouped vertices, " << grouped_faces.size()
              << " grouped faces" << std::endl;

    // The styles which are relevant to this shape. A style is named after the
    // entity class when a default material is applied, otherwise it is named
    // after the surface style it comes from.
    for (auto& style : geometry.materials()) {
        std::cout << style->name << std::endl;
        const auto& colour = style->get_color();
        std::cout << "  diffuse " << colour.r() << ", " << colour.g() << ", " << colour.b() << std::endl;
        if (style->has_transparency()) {
            std::cout << "  transparency " << style->transparency << std::endl;
        }
    }

    // Indices of the material applied per triangle face, e.g. [f1m, f2m, ...],
    // and the representation item each face came from, e.g. [f1i, f2i, ...].
    std::cout << geometry.material_ids().size() << " material ids, "
              << geometry.item_ids().size() << " item ids" << std::endl;

Untriangulated geometry

Alternatively, you may use the untriangulated geometry of the geometry kernel. Here the type of the data depends on the geometry kernel in use.

    // Instead of triangulating the shape, the untriangulated geometry of the
    // kernel can be used. It is a list of shapes, one per representation item.
    std::unique_ptr<ifcopenshell::geom::native_element> brep(
        converter.create_brep_for_representation_and_product(
            converter.mapping()->representation_of(element), element));
    if (!brep) {
        std::cerr << "Shape creation failed" << std::endl;
        return 1;
    }
    std::cout << brep->geometry().shapes().size() << " representation items" << std::endl;

    // The shapes themselves are opaque, so that kernels other than OpenCASCADE
    // can be used, and are combined into a single compound to be able to ask
    // kernel agnostic questions. The compound is allocated, so it should be deleted again.
    ifcopenshell::geom::conversion_result_shape* compound = brep->geometry().as_compound();
    std::cout << "volume " << compound->volume().to_double() << std::endl;
    std::cout << "area " << compound->area().to_double() << std::endl;
    std::cout << compound->num_vertices() << " vertices, " << compound->num_edges()
              << " edges, " << compound->num_faces() << " faces" << std::endl;

The shape is opaque so that kernels other than OpenCASCADE can be used, but when you know the shape was made by the OpenCASCADE kernel it can be cast back to the TopoDS_Shape it wraps, and used with OpenCASCADE directly:

    // If you know the shape was made by the OpenCASCADE kernel, it can be cast
    // back to the TopoDS_Shape it wraps, and used with OpenCASCADE directly.
    // Use compound->backend_id() to test.
    if (auto* occt = dynamic_cast<ifcopenshell::geom::open_cascade_shape*>(compound)) {
        const TopoDS_Shape& topods = occt->shape();
        GProp_GProps properties;
        BRepGProp::VolumeProperties(topods, properties);
        std::cout << "volume " << properties.Mass() << std::endl;
    }

A shape can also be taken apart, in which case each of the sub-shapes can be asked for its own properties:

    // A shape can also be taken apart: facets() gives its faces and vertices()
    // its vertices, and each of those can be asked for its own properties, such
    // as the area() of a face. These shapes are allocated, so the caller deletes
    // them again.
    std::size_t planar_faces = 0;
    for (auto* facet : compound->facets()) {
        // position() is only defined for planar faces, and throws when the face
        // is, for example, part of a cylinder.
        try {
            const auto centre = facet->position().to_double();
            if (planar_faces++ == 0) {
                std::cout << "first face at " << centre[0] << ", " << centre[1] << ", " << centre[2] << std::endl;
            }
        } catch (const std::runtime_error&) {
        }
        delete facet;
    }
    std::cout << planar_faces << " planar faces" << std::endl;

Geometry iterator

IfcOpenShell provides a geometry iterator to efficiently process geometry in an IFC model. The iterator is always used in IfcConvert, and may also be invoked in C++ or in Python. It offers the same features as individual processing, and makes it easy to collect possible geometry in a model, supports multicore processing, and implements caching and reuse to improve the efficiency of geometry processing. For any bulk geometry processing, it is always recommended to use the iterator.

By default, the geometry iterator processes all 3D geometry in a model from all elements, and returns a list of X Y Z vertex ordinates in a flattened list, as well as a flattened list of triangulated faces denoted by vertex indices:

    // The iterator offers the same features as processing a single element, but
    // it collects all geometry in a model, supports multicore processing, and
    // implements caching and reuse to improve the efficiency of geometry
    // processing. For bulk geometry processing, it is always recommended to use
    // the iterator.
    //
    // By default it processes all 3D geometry in a model, from all elements, and
    // returns the vertices as a flattened list, and the triangulated faces as
    // indices into that list.
    ifcopenshell::geom::settings settings;
    const int num_threads = static_cast<int>((std::max)(1u, std::thread::hardware_concurrency()));
    ifcopenshell::geom::iterator it(
        ifcopenshell::geom::kernels::construct(&model, "hybrid-cgal-simple-opencascade", settings),
        settings, &model, num_threads);

    if (it.initialize()) {
        while (true) {
            auto element = it.get();
            // The output is triangulated, unless the IteratorOutput setting says
            // otherwise, so the element is a triangulation_element.
            auto* shape = static_cast<const ifcopenshell::geom::triangulation_element*>(element.get());
            const auto& geometry = shape->geometry();
            std::cout << shape->id() << " " << shape->type() << " " << shape->guid() << " "
                      << geometry.verts().size() / 3 << " vertices, "
                      << geometry.edges().size() / 2 << " edges, "
                      << geometry.faces().size() / 3 << " faces, "
                      << geometry.materials().size() << " styles" << std::endl;
            // ... write code to process the geometry here ...
            if (!it.next()) {
                break;
            }
        }
    }

There are a variety of configuration settings to get different output. For example, you may filter elements from processing, extract 2D data, or return non-triangulated OpenCASCADE BReps. For more information on the various settings, see Geometry Settings.

One of the more common settings used is a filter, which specifies only to process certain geometry. For example, this iterator will only process the windows of the model:

    // One of the more common settings is to include only a subset of the model.
    // Filters are passed to the iterator as a list, and each of them either
    // includes only what it matches, or excludes what it matches.
    std::set<int> window_ids;
    for (auto& window : model.instances_by_type<Ifc4::IfcWindow>()) {
        window_ids.insert(static_cast<int>(window.id()));
    }
    std::vector<ifcopenshell::geom::filter_function> filters;
    filters.push_back(ifcopenshell::geom::instance_id_filter(true, false, window_ids));
    // A filter can also be based on the entity type, which, like elsewhere in the
    // API, includes subtypes, so this would process IfcWallStandardCase as well:
    //   filters.push_back(ifcopenshell::geom::entity_filter(true, false, {"IfcWall"}));

    ifcopenshell::geom::iterator it(
        ifcopenshell::geom::kernels::construct(&model, "hybrid-cgal-simple-opencascade", settings),
        settings, &model, filters, num_threads);

    std::size_t processed = 0;
    if (it.initialize()) {
        while (true) {
            auto element = it.get();
            std::cout << element->id() << " " << element->type() << std::endl;
            ++processed;
            if (!it.next()) {
                break;
            }
        }
    }

Note

The iterator can only be used to process whole elements, not individual shape representations, representation items, and profiles.

Manual parsing

IfcOpenShell lets you traverse any IFC entity graph. This means it is possible for you to manually browse through the Representation attribute of IFC elements, and parse the corresponding IFC representation items yourself instead of using generic geometric processing such as individual processing and the Geometry iterator.

This approach requires an in-depth understanding of IFC geometry representations, as well as its many caveats with units and transformations, but can be very simple and extremely fast to extract specific types of geometry. For example, if you know you are dealing with extrusions, you can specifically pinpoint the depth of the extrusion.

    // IfcOpenShell lets you traverse any IFC entity graph, so the attributes of
    // representation items can be read without a geometry kernel. The caveat is
    // that you then have to deal with the many ways in which IFC can express
    // geometry yourself, including the units of the project and the many nested
    // placements.
    double unit_scale = 1.;
    try {
        // The second member of the pair is the factor to apply to a value in the
        // units of the project, to get its value in SI units.
        unit_scale = model.get_unit("LENGTHUNIT").second;
    } catch (const ifcopenshell::exception&) {
        std::cerr << "No length unit found in the project unit assignment" << std::endl;
    }

    // In the case of an extrusion, the only thing that has to be known
    // beforehand is which profile is swept, and how far.
    std::size_t reported = 0;
    for (auto& solid : model.instances_by_type<Ifc4::IfcExtrudedAreaSolid>()) {
        if (reported++ < 5) {
            const double depth = solid.Depth();
            // In project length units, and in SI meters.
            std::cout << depth << " = " << depth * unit_scale << " m" << std::endl;
        }
    }

Given the advanced nature of manual processing, it is generally not recommended except in specific tasks.

Geometry serialisation

Geometry may be serialised into many different formats using IfcConvert. Alternatively, you may also access the serialiser directly to customise the conversion, such as by writing a program that modifies the IFC on the fly before converting it, or implementing complex include and exclude filters.

Here is a typical example of serialising to glTF / glb, with the settings for obj shown as a comment. Different serialisations may require different settings, and the same settings object also exposes the serialisation options.

    // Geometry may be serialised into many different formats. The settings object
    // is the same one the iterator uses, extended with the settings the serialiser
    // itself supports.
    ifcopenshell::geom::settings settings;
    // Settings for glTF / glb.
    settings.get<ifcopenshell::geom::settings::OutputDimensionality>().value =
        ifcopenshell::geom::settings::CURVES_SURFACES_AND_SOLIDS;
    // Note that applying default materials is required in glTF serialisation.
    settings.get<ifcopenshell::geom::settings::ApplyDefaultMaterials>().value = true;
    // Settings for obj, which are serialised to world coordinates instead.
    //   settings.get<ifcopenshell::geom::settings::UseWorldCoords>().value = true;
    // Setting element GUIDs is optional, but useful to uniquely identify objects
    // in non semantic formats.
    settings.get<ifcopenshell::geom::settings::UseElementGuids>().value = true;

    // The serialisers are plugins, which are found next to the IfcOpenShell
    // libraries, and are addressed by the extension of the file to write. This
    // example writes output.glb in the current directory.
    ifcopenshell::serializers::geometry_serializer_context context{"output.glb", "output.glb", settings};
    auto& registry = ifcopenshell::serializers::geometry_serializer_registry_instance();
    registry.configure(".glb", context);
    auto serializer = registry.create(".glb", context);
    // To serialise to obj instead:
    //   auto serializer = registry.create(".obj", context);

    serializer->setFile(model);
    // Without the ConvertBackUnits setting, the geometry is in meters already.
    serializer->setUnitNameAndMagnitude("METER", 1.0f);
    serializer->writeHeader();

    ifcopenshell::geom::iterator it(
        ifcopenshell::geom::kernels::construct(&model, "hybrid-cgal-simple-opencascade", settings),
        settings, &model, static_cast<int>((std::max)(1u, std::thread::hardware_concurrency())));
    if (it.initialize()) {
        while (true) {
            auto element = it.get();
            serializer->write(
                static_cast<const ifcopenshell::geom::triangulation_element*>(element.get()));
            if (!it.next()) {
                break;
            }
        }
    }
    serializer->finalize();

Note

The serialisers are plugins which are discovered next to the IfcOpenShell libraries, so a serialiser for a format is only available when it was built and installed.