在 c++ 中构建封装但可扩展的动画库

Building an encapsulated but extensible animation library in c++

本文关键字:可扩展的 动画 封装 c++ 构建      更新时间:2023-10-16

我正在用c ++构建一个动画库。 该库将包括一个用于建模和渲染场景的系统。 系统的要求是

  1. 建模和渲染分离。 有关场景状态的信息应与渲染场景的过程分开存储。
  2. 可扩展的建模和渲染。 如果库本身定义了node类,则库的用户应该能够定义一个新的类型custom_node来扩展node的功能(可能通过继承,但也可能通过其他方式(。 然后,用户应该能够指定用于呈现custom_node的自定义过程。 在这样做时,用户应该能够以某种方式利用库中已经存在的呈现过程。 用户还应该能够定义渲染库节点的新过程。另外:用户应该能够定义整个渲染系统,并选择使用哪一个来渲染场景。 例如,假设该库包含一个逼真的渲染系统,但用户希望使用准系统示意图渲染系统渲染场景。 用户应该能够使用动画库在动画循环期间在后台使用的通用渲染接口来实现这样的渲染器(渲染帧、更新场景、渲染下一帧等(。
  3. 库的封装。若要将库的功能扩展到自定义node和呈现过程,用户无需编辑库的基础代码。

失败的方法:使用 node 的树作为场景的模型。 子类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 个需求的系统呢?

键入擦除。 该库提供渲染(some_data(函数。

我们从几种节点开始。 基元是渲染(基元(只是绘制某些东西的节点。

列表节点有子节点,渲染(list_node(绘制其内容。

generic_node存储具有渲染(?(重载的任何内容。 它类型擦除渲染(?(操作。 调用 render(generic_node( 会调用对所包含数据的类型擦除操作。

list_node包含 generic_nodes 向量。

为了添加新的渲染类型,您只需定义一个新类型,重载渲染(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 提出的类型擦除方法的变体。 可以在此处找到有关该问题和此特定方法的更多详细信息。

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*/};
};