Я строю библиотеку анимации в C ++. Библиотека будет включать систему моделирования и рендеринга сцен. Требования системы
node
класс, пользователь библиотеки должен иметь возможность определить новый тип custom_node
что расширяет функциональность node
(возможно, по наследству, но, возможно, с помощью других средств). Затем пользователь должен иметь возможность указать пользовательскую процедуру для рендеринга custom_node
, При этом пользователь должен каким-то образом иметь возможность воспользоваться процедурами рендеринга, уже присутствующими в библиотеке. Пользователь также должен иметь возможность определять новые процедуры для рендеринга библиотечных узлов. прибавление: пользователь должен иметь возможность определять целые системы рендеринга и выбирать, какую из них использовать для рендеринга сцены. Предположим, например, что библиотека включает в себя фотореалистичную систему рендеринга, но пользователь хочет рендерить сцены с помощью базовой схемы рендеринга. Пользователь должен иметь возможность реализовать такой рендерер, используя общий интерфейс рендеринга, который библиотека анимации использует под капотом во время цикла анимации (рендеринг кадра, обновление сцены, рендеринг следующего кадра и т. Д.).node
s и процедуры рендеринга, пользователь не должен редактировать основной код библиотеки.Неудачный подход: используйте дерево 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 требованиям?
Тип стирания. Библиотека предоставляет функции render (some_data).
Мы начнем с нескольких видов узлов. Примитивы — это узлы, которые визуализируют (примитив) просто что-то рисует.
Узлы списка имеют дочерние элементы, а render (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) };
}
Как насчет узла, который печатает привет мир при рендеринге?
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);
Общие узлы — это неизменяемые типы значений, основанные на разделяемом указателе, которые при копировании выполняют незначительную работу.
Мое собственное решение, вариант метода стирания типа, предложенный Якком. Более подробную информацию о проблеме и этот конкретный подход можно найти Вот.
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*/};
};