用c++构建一个封装但可扩展的动画库
Building an encapsulated but extensible animation library in c++
我正在用 C++ 构建一个动画库。该库将包括一个用于建模和渲染场景的系统。系统的要求是
- 建模渲染分离。有关场景状态的信息应与渲染场景的过程分开存储。
- 可扩展的建模和渲染。如果库本身定义了
node
class,库的用户应该能够定义一个新类型 custom_node
来扩展 node
的功能(可能通过继承,但也许通过其他方式)。然后用户应该能够指定自定义过程来呈现 custom_node
。这样做时,用户应该能够以某种方式利用库中已有的渲染程序。用户还应该能够定义用于呈现库节点的新过程。 补充:用户应该能够定义整个渲染系统和select使用哪个来渲染场景。例如,假设该库包含一个逼真的渲染系统,但用户想要使用准系统原理图渲染系统渲染场景。用户应该能够使用动画库在动画循环(渲染帧、更新场景、渲染下一帧等)期间使用的公共渲染接口来实现这样的渲染器。
- 库的封装。要将库的功能扩展到自定义
node
s 和渲染程序,用户不需要编辑底层代码图书馆。
一个失败的方法:用一棵node
的树作为一个场景的模型。 Subclass node
创建新的节点类型。由于节点的子节点的类型可能直到运行时才知道,因此节点的子节点存储在 vector<std::shared_ptr<node>>
中。
还定义了顶层 renderer
class 和 subclass renderer
以提供特定种类的渲染。
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 */}
// ...
}
要渲染场景,定义渲染器
renderer r{};
并用你最喜欢的遍历方法遍历节点树。当你遇到每个 std::shared_ptr<node>
n
时,调用
n->render(r);
这种方法将建模和渲染分开,并且允许扩展。要创建 custom_node
,库的用户只需 subclasses node
class custom_node : public node {
virtual image render(renderer &r) override {return r.render(*this)}
}
在我们尝试提供一种自定义方法来呈现我们的 custom_node
之前,这种方法工作正常。为此,我们尝试 subclassing renderer
并重载 render
方法:
class custom_renderer : public renderer {
image render(custom_node &n) {/*custom rendering code*/}
}
这本身是行不通的。考虑:
renderer &r = custom_renderer{};
std::shared_ptr<node> n = std::make_shared<custom_node>{};
n->render(r); // calls renderer::render(node &)
为了根据需要调用custom_renderer::render(custom_node &n),我们需要为我们的原始渲染器添加一个虚拟重载class:
class renderer {
image render(node &n) {/*rendering code */}
virtual image render(custom_node &n) = 0;
}
不幸的是,这破坏了库的封装,因为我们已经编辑了其中一个库 classes.
那么,我们如何设计一个满足所有 3 个要求的系统?
类型擦除。该库提供 render(some_data) 函数。
我们从几种节点开始。图元是渲染(图元)只是绘制一些东西的节点。
列表节点有子节点,render(list_node) 绘制其内容。
generic_node 存储任何具有 render(?) 重载的内容。它类型擦除 render(?) 操作。调用 render(generic_node) 会对包含的数据调用类型擦除操作。
list_node 包含 generic_node 的向量。
为了添加新的渲染类型,您只需定义一个新类型,重载 render(new_type),然后将其存储在 generic_node.
中
这是一个原始实现:
struct render_target {
// stuff about the thing we are rendering on
};
struct renderable_concept {
virtual ~renderable_concept() {}
virtual void render_on( render_target* ) const = 0;
};
template<class T>
void render( render_target*, T const& ) = delete; // by default, nothing renders
struct emplace_tag {};
template<class T>
struct renderable_model : renderable_concept {
T t;
template<class...Us>
renderable_model( emplace_tag, Us&&...us ):
t{std::forward<Us>(us)...}
{}
void render_on( render_target* target ) const final override {
render( target, t );
}
};
template<class T>
struct emplace_as {};
struct generic_node {
friend void render( render_target* target, generic_node const& node ) {
if (!node.pImpl) return;
node.pImpl->render_on(target);
}
template<class T, class...Us>
generic_node( emplace_as<T>, Us&&... us):
pImpl( std::make_shared<renderable_model<T>>(emplace_tag{}, std::forward<Us>(us)...) )
{}
generic_node() = default;
generic_node(generic_node&&)=default;
generic_node(generic_node const&)=default;
generic_node& operator=(generic_node&&)=default;
generic_node& operator=(generic_node const&)=default;
private:
std::shared_ptr<renderable_concept> pImpl;
};
现在,如何制作列表节点。
struct list_node {
std::vector<generic_node> nodes;
friend void render( render_target* target, list_node const& self ) {
for (auto&& node:self.nodes)
render(target, node);
}
list_node(std::vector<generic_node> ns):nodes(std::move(ns)) {}
list_node() = default;
list_node(list_node&&)=default;
list_node& operator=(list_node&&)=default;
};
template<class T, class...Args>
generic_node make_node( Args&&... args ) {
return {emplace_as<T>{}, std::forward<Args>(args)...};
}
template<class T>
generic_node make_node( T&& t ) {
return {emplace_as<std::decay_t<T>>{}, std::forward<T>(t) };
}
渲染时打印 hello world 的节点如何?
struct printing_node {
std::string message;
friend void render( render_target* target, printing_node const& self ) {
std::cout << self.message;
}
};
测试代码:
auto list = make_node( list_node{{
make_node( printing_node{{"hello"}} ),
make_node( printing_node{{"world"}} )
}});
render_target target;
render(&target, list);
通用节点是基于共享指针的不可变值类型,在复制时几乎不做任何工作。
我自己的解决方案,是 Yakk 提出的类型擦除方法的变体。有关此问题和此特定方法的更多详细信息,请参见 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*/};
};
我正在用 C++ 构建一个动画库。该库将包括一个用于建模和渲染场景的系统。系统的要求是
- 建模渲染分离。有关场景状态的信息应与渲染场景的过程分开存储。
- 可扩展的建模和渲染。如果库本身定义了
node
class,库的用户应该能够定义一个新类型custom_node
来扩展node
的功能(可能通过继承,但也许通过其他方式)。然后用户应该能够指定自定义过程来呈现custom_node
。这样做时,用户应该能够以某种方式利用库中已有的渲染程序。用户还应该能够定义用于呈现库节点的新过程。 补充:用户应该能够定义整个渲染系统和select使用哪个来渲染场景。例如,假设该库包含一个逼真的渲染系统,但用户想要使用准系统原理图渲染系统渲染场景。用户应该能够使用动画库在动画循环(渲染帧、更新场景、渲染下一帧等)期间使用的公共渲染接口来实现这样的渲染器。 - 库的封装。要将库的功能扩展到自定义
node
s 和渲染程序,用户不需要编辑底层代码图书馆。
一个失败的方法:用一棵node
的树作为一个场景的模型。 Subclass node
创建新的节点类型。由于节点的子节点的类型可能直到运行时才知道,因此节点的子节点存储在 vector<std::shared_ptr<node>>
中。
还定义了顶层 renderer
class 和 subclass renderer
以提供特定种类的渲染。
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 */}
// ...
}
要渲染场景,定义渲染器
renderer r{};
并用你最喜欢的遍历方法遍历节点树。当你遇到每个 std::shared_ptr<node>
n
时,调用
n->render(r);
这种方法将建模和渲染分开,并且允许扩展。要创建 custom_node
,库的用户只需 subclasses node
class custom_node : public node {
virtual image render(renderer &r) override {return r.render(*this)}
}
在我们尝试提供一种自定义方法来呈现我们的 custom_node
之前,这种方法工作正常。为此,我们尝试 subclassing renderer
并重载 render
方法:
class custom_renderer : public renderer {
image render(custom_node &n) {/*custom rendering code*/}
}
这本身是行不通的。考虑:
renderer &r = custom_renderer{};
std::shared_ptr<node> n = std::make_shared<custom_node>{};
n->render(r); // calls renderer::render(node &)
为了根据需要调用custom_renderer::render(custom_node &n),我们需要为我们的原始渲染器添加一个虚拟重载class:
class renderer {
image render(node &n) {/*rendering code */}
virtual image render(custom_node &n) = 0;
}
不幸的是,这破坏了库的封装,因为我们已经编辑了其中一个库 classes.
那么,我们如何设计一个满足所有 3 个要求的系统?
类型擦除。该库提供 render(some_data) 函数。
我们从几种节点开始。图元是渲染(图元)只是绘制一些东西的节点。
列表节点有子节点,render(list_node) 绘制其内容。
generic_node 存储任何具有 render(?) 重载的内容。它类型擦除 render(?) 操作。调用 render(generic_node) 会对包含的数据调用类型擦除操作。
list_node 包含 generic_node 的向量。
为了添加新的渲染类型,您只需定义一个新类型,重载 render(new_type),然后将其存储在 generic_node.
中这是一个原始实现:
struct render_target {
// stuff about the thing we are rendering on
};
struct renderable_concept {
virtual ~renderable_concept() {}
virtual void render_on( render_target* ) const = 0;
};
template<class T>
void render( render_target*, T const& ) = delete; // by default, nothing renders
struct emplace_tag {};
template<class T>
struct renderable_model : renderable_concept {
T t;
template<class...Us>
renderable_model( emplace_tag, Us&&...us ):
t{std::forward<Us>(us)...}
{}
void render_on( render_target* target ) const final override {
render( target, t );
}
};
template<class T>
struct emplace_as {};
struct generic_node {
friend void render( render_target* target, generic_node const& node ) {
if (!node.pImpl) return;
node.pImpl->render_on(target);
}
template<class T, class...Us>
generic_node( emplace_as<T>, Us&&... us):
pImpl( std::make_shared<renderable_model<T>>(emplace_tag{}, std::forward<Us>(us)...) )
{}
generic_node() = default;
generic_node(generic_node&&)=default;
generic_node(generic_node const&)=default;
generic_node& operator=(generic_node&&)=default;
generic_node& operator=(generic_node const&)=default;
private:
std::shared_ptr<renderable_concept> pImpl;
};
现在,如何制作列表节点。
struct list_node {
std::vector<generic_node> nodes;
friend void render( render_target* target, list_node const& self ) {
for (auto&& node:self.nodes)
render(target, node);
}
list_node(std::vector<generic_node> ns):nodes(std::move(ns)) {}
list_node() = default;
list_node(list_node&&)=default;
list_node& operator=(list_node&&)=default;
};
template<class T, class...Args>
generic_node make_node( Args&&... args ) {
return {emplace_as<T>{}, std::forward<Args>(args)...};
}
template<class T>
generic_node make_node( T&& t ) {
return {emplace_as<std::decay_t<T>>{}, std::forward<T>(t) };
}
渲染时打印 hello world 的节点如何?
struct printing_node {
std::string message;
friend void render( render_target* target, printing_node const& self ) {
std::cout << self.message;
}
};
测试代码:
auto list = make_node( list_node{{
make_node( printing_node{{"hello"}} ),
make_node( printing_node{{"world"}} )
}});
render_target target;
render(&target, list);
通用节点是基于共享指针的不可变值类型,在复制时几乎不做任何工作。
我自己的解决方案,是 Yakk 提出的类型擦除方法的变体。有关此问题和此特定方法的更多详细信息,请参见 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*/};
};