FEP00 | |
---|---|
Title | Evolution of FreeCAD's Rendering Subsystem |
Status | Draft |
Author(s) | Joao Matos (tritao) |
Created | Mar 02, 2025 |
Updated | Mar 02, 2025 |
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.
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:
- Geometry provider
- Hierarchical Transforms
- Math graphics library
- Rendering provider
- Bounding volume hierarchy functionality
It also provides an event system, which is currently used to handle view-related functionality, like selection.
There are some problems related to Coin itself, as well as to how FreeCAD uses Coin, which we will explore below.
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.
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.
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.
Coin does not provide any modern rendering features like integrated shadows and lighting models. Global and indirect lighting algorithms are missing.
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.
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);
}
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.
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).
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.
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:
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.
- bgfx GitHub Repository
- sokol GitHub Repository
- SDL 3.0 GPU Documentation
- DiligentEngine GitHub Repository
- Qt Rendering Hardware Interface Documentation
- OGRE3D Features
- Godot Engine Features
- Filament GitHub Repository
- Panda3D Features
- rbfx GitHub Repository
- Godot PR #90510
All FEPs are explicitly CC0 1.0 Universal.