我正在用 C++ 构建一个动画库。该库将包括一个用于建模和渲染场景的系统。系统的要求是
- 建模与渲染分离。有关场景状态的信息应与渲染场景的过程分开存储。
- 可扩展的建模和渲染。如果库本身定义了一个
node
类,则该库的用户应该能够定义一个custom_node
扩展其功能的新类型node
(可能通过继承,但也可能通过其他方式)。然后,用户应该能够指定一个自定义过程来呈现custom_node
. 在这样做时,用户应该能够以某种方式利用库中已经存在的渲染过程。用户还应该能够定义用于渲染库节点的新过程。添加:用户应该能够定义整个渲染系统并选择使用哪个渲染场景。例如,假设该库包含一个逼真的渲染系统,但用户想要使用准系统示意图渲染系统来渲染场景。用户应该能够使用动画库在动画循环(渲染帧、更新场景、渲染下一帧等)期间使用的通用渲染接口来实现这样的渲染器。 - 库的封装。要将库的功能扩展到自定义
node
和渲染过程,用户不需要编辑库的底层代码。
一种失败的方法:使用node
s 的树作为场景的模型。node
创建新节点类型的子类。由于节点的子节点的类型可能要到运行时才能知道,所以节点的子节点存储在vector<std::shared_ptr<node>>
. 还定义一个顶级renderer
类和子类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
,库的用户只需子类node
class custom_node : public node {
virtual image render(renderer &r) override {return r.render(*this)}
}
在我们尝试提供一种自定义方式来呈现我们的custom_node
. 为此,我们尝试子类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 renderer {
image render(node &n) {/*rendering code */}
virtual image render(custom_node &n) = 0;
}
不幸的是,这破坏了库的封装,因为我们已经编辑了其中一个库类。
那么,我们如何设计一个满足所有 3 个要求的系统呢?