用c++构建一个封装但可扩展的动画库

Building an encapsulated but extensible animation library in c++

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

  1. 建模渲染分离。有关场景状态的信息应与渲染场景的过程分开存储。
  2. 可扩展的建模和渲染。如果库本身定义了 node class,库的用户应该能够定义一个新类型 custom_node 来扩展 node 的功能(可能通过继承,但也许通过其他方式)。然后用户应该能够指定自定义过程来呈现 custom_node。这样做时,用户应该能够以某种方式利用库中已有的渲染程序。用户还应该能够定义用于呈现库节点的新过程。 补充:用户应该能够定义整个渲染系统和select使用哪个来渲染场景。例如,假设该库包含一个逼真的渲染系统,但用户想要使用准系统原理图渲染系统渲染场景。用户应该能够使用动画库在动画循环(渲染帧、更新场景、渲染下一帧等)期间使用的公共渲染接口来实现这样的渲染器。
  3. 库的封装。要将库的功能扩展到自定义 nodes 和渲染程序,用户不需要编辑底层代码图书馆。

一个失败的方法:用一棵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);

Live example.

通用节点是基于共享指针的不可变值类型,在复制时几乎不做任何工作。

我自己的解决方案,是 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*/};
};