Skip to content

Instantly share code, notes, and snippets.

@tritao
Last active March 2, 2025 14:45
Show Gist options
  • Save tritao/3ca2908a5887235550c49340a14078a0 to your computer and use it in GitHub Desktop.
Save tritao/3ca2908a5887235550c49340a14078a0 to your computer and use it in GitHub Desktop.
FEP00
Title Evolution of FreeCAD's Rendering Subsystem
Status Draft
Author(s) Joao Matos (tritao)
Created Mar 02, 2025
Updated Mar 02, 2025

Abstract

This proposal addresses the evolution of FreeCAD's rendering subsystem. It examines the current architecture based on Coin3D, identifies key issues, such as performance bottlenecks, limited functionality, and portability challenges, and explores potential solutions including the adoption of modern graphics abstraction libraries, extending the FreeCAD object model, and integrating higher-level rendering engines. The goal is to open a discussion with the broader FreeCAD community and reach a consensus on the future direction.


Overview

FreeCAD provides rendering via the Coin3D library, which is an OpenGL-based, 3D graphics library that has its roots in the Open Inventor 2.1 API, which it still is compatible with.

It implements a classically architected scene graph, consisting of a tree with different kinds of linked nodes, which are mainly representing geometry, grouping, transformation, as well as setting up rendering state.

Coin is currently used inside FreeCAD for:

  1. Geometry provider
  2. Hierarchical Transforms
  3. Math graphics library
  4. Rendering provider
  5. Bounding volume hierarchy functionality

It also provides an event system, which is currently used to handle view-related functionality, like selection.


Existing Issues

There are some problems related to Coin itself, as well as to how FreeCAD uses Coin, which we will explore below.

1. Performance

This is probably the main reason that started me on this and its just that FreeCAD's rendering is very slow.

Rendering walks the scene graph multiple times, which is very slow. With a modern design, we should be able to not need to walk the scene graph for rendering each frame.

Lack of Instancing

Modern GPUs provide instancing support, which allows rendering similar objects with a single draw call, which is a lot more efficient than individually submitting to the GPU, as the geometry only needs to be submitted once to the GPU, while also sending a buffer with just the instance-specific attributes.

In FreeCAD, this should be done where links are used, but at the moment it is not, leading to big performance problems when there's dozens of hundreds of links to the same object.


2. Portability

Coin is currently stuck with OpenGL 1.5-level functionality; there is support for shaders but nothing really uses them, and documentation is very scarce.

This is an issue when porting to modern systems which use modern graphical APIs like Vulkan and Metal. This is both a Coin issue, as well as a FreeCAD issue, because FreeCAD itself uses immediate mode OpenGL style calls, which are not compatible with modern OpenGL rendering.


3. Limited Functionality

Coin does not provide any modern rendering features like integrated shadows and lighting models. Global and indirect lighting algorithms are missing.


Limited Python Bindings Support

It's not possible to create new Coin nodes from Python, which is a pretty big expressivity problem compared to C++.

This limits developers using Python to only re-use existing C++-defined Coin nodes, which limits what can be done from Python, leading to workbenches being limited to what can be achieved.

Coin bindings are provided as part of the Pivy project which is maintained and released separately from the main Coin releases which can also be an issue if improvements in Coin are to be made.

4. Architecture Problems

From a technical perspective, Coin old-school design is also problematic in a number of key areas.

It for example supports an action-based system for working with nodes, which is implemented with the doAction callback at the node-level:

class SoNode {
    // ...
    virtual void SoNode::doAction(SoAction * action);
    // ...
}

This is not ideal as nodes need to hardcode the code for specific external actions in their internal implementation, which heavily limits external users like FreeCAD from implementing their own actions on builtin Coin scene graph nodes, leading in some cases to code duplication in FreeCAD.

Additionally, due to this design, nodes need to explicitly enable which elements are needed for each action.

For example, here is an example from Coin where such pattern is shown:

void SoSeparator::initClass(void) {
  // ...
  SO_ENABLE(SoGetBoundingBoxAction, SoCacheElement);
  SO_ENABLE(SoGLRenderAction, SoCacheElement);
}

Potential Solutions

Using Coin3D as a Scene Provider and Rendering with Graphics Abstraction Library

This solution keeps using Coin3D as the scene graph and primitive geometry provider, and switches out the internal rendering layer to a modern cross-platform graphics abstraction library. Suitable free software options include:

There are tradeoffs for each option, but overall the particular choice is less critical than the overall approach.

FreeCAD would continue using Coin as the main scene rendering layer, converting the representation inside the Coin3D tree into an equivalent representation for the graphics abstraction library. This representation would be cached, allowing geometry to be submitted to the GPU much more efficiently.

This has the big advantage that the entire ecosystem won't require immediate changes and full backwards compatibility can be guaranteed.

It also doesn't impact any future migration to another higher-level rendering engine, if anything it would simplify any such migration in the future, as a lot of code inside FreeCAD needs to be updated out to work with an abstract rendering layer.


Using a High-Level Rendering Engine for Scene and Rendering Providers

There are higher-level rendering engines which sit above graphics abstraction libraries and provide advanced features such as:

  • Physically-based rendering
  • Real-time lighting models (with area lights)
  • Shadow mapping
  • Global illumination with indirect lighting
  • Antialiasing

Additional features potentially useful for robotics simulation, BIM visualization, or VR include:

  • Reflections
  • Decals
  • Sky
  • Fog
  • Volumetric fog
  • Particles
  • Post-processing

Suitable free software high-level engines include:

Special mention goes to the Godot engine, which has recently made steps towards supporting the use case described here (see Godot PR #90510).


Extending FreeCAD's Object Model to a Scene Graph

Another interesting option is extending FreeCAD's object model to provide the scene graph functionality necessary.

This does not necessarily compete with the solutions above, it just means FreeCAD itself can implement a subset of the functionality that is currently implemented by Coin, like the bounding volume hierarchy or keeping track of transforms.

FreeCAD object model is already a tree, and already contains placements and bounding boxes, so this would be quite a natural and minimal extension.

It would greatly simplify the Python bindings layer as this functionality could be provided by the existing bindings system.

Some more detailed discussion is necessary to nail out the full details, but at least this seems a very viable option that should be considered alongside the other approaches.


Technical Details

This section explores how each approach discussed above can be integrated into FreeCAD's current rendering architecture, which is based on the ViewProvider class.

Each view provider implements a set of overloads that return the associated Coin3D nodes:

class ViewProvider : public App::TransactionalObject
{
    // ...

    // returns the root node of the Provider (3D)
    virtual SoSeparator* getRoot() const {return pcRoot;}
    // return the mode switch node of the Provider (3D)
    SoSwitch *getModeSwitch() const {return pcModeSwitch;}
    SoTransform *getTransformNode() const {return pcTransform;}
    // returns the root for the Annotations.
    SoSeparator* getAnnotation();
    // returns the root node of the Provider (3D)
    virtual SoSeparator* getFrontRoot() const;
    // returns the root node where the children gets collected (3D)
    virtual SoGroup* getChildRoot() const;
    // returns the root node of the Provider (3D)
    virtual SoSeparator* getBackRoot() const;
    /// Indicate whether to be added to scene graph or not
    virtual bool canAddToSceneGraph() const {return true;}
    // Indicate whether to be added to object group (true) or only to scene graph (false)
    virtual bool isPartOfPhysicalObject() const {return true;}

    // ...
}

Modern rendering pipelines are designed so that submitting geometry to the GPU is done as fast as possible, typically by using simple lists of draw calls that can be linearly iterated and processed—much faster than pointer-based tree iteration.

A draw call is a command issued by the CPU to the GPU instructing it to render a set of primitives (such as triangles, lines, or points) with specific state settings. Here’s a brief breakdown:

  • Communication from CPU to GPU: The CPU sends a draw call to instruct the GPU to render a batch of vertices with current settings.
  • State and Resource Binding: Before a draw call, various states (shaders, textures, blending modes, etc.) are set. The draw call then uses these settings to process the vertex and fragment data.
  • Batching and Performance: Minimizing the number of draw calls by batching similar objects together is a common optimization.

For example, taking bgfx as a case study, the ViewProvider interface can be extended with bgfx's DrawCallBuilder which can be used for submitting geometry to the GPU:

In this case, a submitCoinNode node is used, that would bridge the Coin internal geometry representation and translate it into BGFX representation.

But it would also allow non-Coin geometry to be submitted for cases where maximum performance is necessary (like instanced geometry, for links).

class ViewProvider {
    // ...
    SoNode *node;

    void submitGeometry(bgfx::DrawCallBuilder& b) {
        b.submitCoinNode(node);
    }
    // ...
}

So overall each view provider would manage its own rendering state, including:


Resolution

The decision to proceed with evolving the rendering subsystem will depend on community feedback, prototype results, and an assessment of long-term benefits versus transition costs. A consensus-driven approach will be taken to ensure that any adopted solution meets the performance and functional needs of the FreeCAD user base.


Reference


Copyright

All FEPs are explicitly CC0 1.0 Universal.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment