I am building an animation library in c++. The library will include a system for modeling and rendering scenes. The requirements of the system are
node
class, a user of the library should be able to define a new type custom_node
that extends the functionality of node
(probably through inheritance, but maybe through some other means). The user should then be able specify a custom procedure for rendering a custom_node
. In doing so, the user should somehow be able to take advantage of rendering procedures already present in the library. The user should also be able to define new procedures for rendering library nodes. Addition: the user should be able to define whole rendering systems and select which one to use to render a scene. Suppose, for instance, that the library includes a photorealistic rendering system, but a user wants to render scenes with a barebones schematic rendering system. The user should be able to implement such a renderer using a common rendering interface that the animation library uses under the hood during the animation loop (render frame, update scene, render next frame, etc).node
s and rendering procedures, a user shouldn't need to edit the underlying code of the library.A failed approach: use a tree of node
s as the model of a scene. Subclass node
to make new node types. Since the types of the children of a node may not be known until runtime, a node’s children are stored in a vector<std::shared_ptr<node>>
.
Also define a top level renderer
class, and subclass renderer
to provide specific kinds of rendering.
class image;
class node {
virtual image render(renderer &r) {return r.render(*this);}
std::vector<std::shared_ptr<node>> children;
std::weak_ptr<node> parent;
// ...
}
class renderer {
image render(node &n) {/*rendering code */}
// ...
}
To render a scene, define a renderer
renderer r{};
and traverse the tree of nodes with your favorite traversal method. As you encounter each std::shared_ptr<node>
n
, call
n->render(r);
This approach separates modeling and rendering, and it allows for extensibility. To create a custom_node
, a user of the library simply subclasses node
class custom_node : public node {
virtual image render(renderer &r) override {return r.render(*this)}
}
This approach works fine until we try to provide a custom means of rendering our custom_node
. To do so, we try subclassing renderer
and overloading the render
method:
class custom_renderer : public renderer {
image render(custom_node &n) {/*custom rendering code*/}
}
By itself, this doesn’t work. Consider:
renderer &r = custom_renderer{};
std::shared_ptr<node> n = std::make_shared<custom_node>{};
n->render(r); // calls renderer::render(node &)
In order to call custom_renderer::render(custom_node &n), as desired, we need to add a virtual overload to our original renderer class:
class renderer {
image render(node &n) {/*rendering code */}
virtual image render(custom_node &n) = 0;
}
Unfortunately, this destroys the encapsulation of the library because we have edited one of the library classes.
How, then, can we design a system that satisfies all 3 requirements?
My own solution, a variant on the type-erasure method proposed by Yakk. More details on the problem and this specific approach can be found here.
struct image{};
struct renderable_concept {
virtual image render() const = 0;
};
template <class WRAPPED, class RENDERER>
struct renderable_model : public renderable_concept {
WRAPPED *w;
RENDERER r;
virtual image render() const final override {
return r.render(*w);
}
renderable_model(WRAPPED *w_, RENDERER r_) : w(w_), r(r_) {}
};
struct node {
template <class WRAPPED, class RENDERER>
node(WRAPPED *w_, RENDERER r_) :
p_renderable(new renderable_model<WRAPPED,RENDERER>(w_,r_)) {}
template <class RENDERER>
node(RENDERER r_) : node(this,r_) {}
image render() {return p_renderable->render();}
vector<shared_ptr<node>> children;
unique_ptr<renderable_concept> p_renderable;
};
struct text_node : public node {
template<class RENDERER>
text_node(RENDERER r) : node(this,r) {}
string val;
};
struct shape_node : public node {
template<class RENDERER>
shape_node(RENDERER r) : node(this,r) {}
};
struct color_renderer {
image render(node &) const {/*implementation*/};
image render(text_node &) const {/*implementation*/};
image render(shape_node &) const {/*implementation*/};
};
struct grayscale_renderer {
image render(node &) const {/*implementation*/};
image render(text_node &) const {/*implementation*/};
image render(shape_node &) const {/*implementation*/};
};