Basic Concept

From Ciliz|W4

Scope

В статье описаны основные решения, используемые в W4 Game Engine, и их API.

Coordinate System

Координатная система

Используется left-handed координатная система и вращение производится против часовой стрелки, если смотреть в направлении оси.

Render Tree

Отображаемый на экране результат и зависимость трансформаций объектов друг от друга определяются деревом рендеринга, то есть набором объектов рендеринга. Особенности дерева рендеринга W4 Game Engine:

  • Минимальное дерево рендеринга состоит только из корневого узла.
  • У каждого узла может быть ноль или более подузлов — «детей», но при этом у узла не может быть более одного “родителя“ (самый верхний узел - “корневой“ - родителя не имеет).
  • Для того чтобы объект отобразился на экране и/или пересчитал свои данные, необходимо чтобы он находился в дереве.
  • Иерархия встроенных типов узлов показана на схеме.

Concept 02.png

Примечания по использованию:

  • При рисовании в несколько проходов, у каждого прохода создаётся своё дерево со своим корневым узлом.
  • У каждого узла можно узнать локальные трансформации (то есть трансформации относительно “родителя“) и мировые, а также задать их.
  • При задании мировых координат узла пересчитываются его локальные координаты, при задании локальных - мировые.
  • У узла, не имеющего родителя трансформации совпадают.
  • Можно временно отключить узел, при этом отключаются и все его “потомки“.

Интерфейсы узлов (классов) описаны далее.

Node

Node - базовый класс любого узла (в том числе RootNode).

Метод Описание
w4::sptr<render::RootNode> getRoot() const
Возвращает корневой узел дерева или nullptr при её отсутствии.
w4::sptr<Node> clone() const
Создаёт копию узла, но не имеющую “родителя“
w4::sptr<Node> getParent() const
Возвращает “родительский“ узел или nullptr при её отсутствии.
void setEnabled(bool)
Включает/выключает узел и его “потомков“
void addChild(w4::cref<Node>, bool preserveTransorm = true)

void addChild(const std::string&, w4::cref<Node>, bool preserveTransorm = true)
Добавляет “детей“ к текущему узлу. Возможно задание имени для “ребёнка“.

Параметр "preserveTransform" определяет локальные(false) или глобальные(true) трансформации, которые сохраняются у “ребёнка“.

Например, если дополнить код слева строками:

childNode->setTranslation({1, 0, 0});
parentNode->addChild(childNode);

то узел childNode будет находиться в мировой позиции {1, 0, 0},

а если использовать следующие строки:

childNode->setTranslation({1, 0, 0});
parentNode->addChild(childNode, false);

то узел childNode будет сдвинут на 1 по оси X относительно родителя.

void removeChild(w4::cref<Node>)

void removeChild(const std::string&)

void removeChild(const std::list<w4::sptr<Node>>&)
Отвязка “детей“ от узла по указателю, имени и списку.
void traversal(const Callback&)

 void traversal(const PredicateC & predicate, const Callback&)

template<typename T> void traversalTyped(const std::function<void(w4::cref<T>)>&);
Выполнение функтора начиная с текущего узла и вниз по иерархии.

Callback имеет сигнатуру void(Node&) PredicateC - bool(w4::core::Node&)

Последний метод вызывается только для узлов, чей тип унаследован от T

void setWorldRotation(math::Rotator::cref, math::vec3::cref worldPt)
Установка поворота относительно точки в мировых координатах.
void rotateAroundPoint(const math::Rotator& rotator, const math::vec3& worldPt)
Вращение вокруг точки в мировых координатах.
template<typename T> w4::sptr<T> as()

template<typename T> w4::sptr<const T> as()   const

template<typename T> T* asRaw()

template<typename T> const T* asRaw() const
Приведение типа узла (прим. указываемый тип не проверяется)
template<typename T> bool derived_from() const
Проверка, является ли класс узла потомком заданного класса
template<typename T> w4::sptr<T> getChild(const std::string&)
Получение ”ребёнка” по имени с приведением типа (прим. указываемый тип не проверяется).
std::list<w4::sptr<Node>> getAllChildren() const
Получение списка всех узлов, находящихся ниже по иерархии (“дети“, “дети детей“…).
std::list<w4::sptr<Node>> findChildren(std::string const&) const
Поиск “ребёнка“ по имени
std::list<w4::sptr<Node>> findChildrenRecursive(std::string const&) const
Поиск вниз по иерархии по имени
math::vec3::cref getWorldTranslation() const

void setWorldTranslation(math::vec3::cref)

math::vec3::cref getLocalTranslation() const

void setLocalTranslation(math::vec3::cref)
Получение и установка позиции в мировых или локальных координатах
math::vec3::cref getWorldScale() const

void setWorldScale(math::vec3::cref)

math::vec3::cref getLocalScale() const

void setLocalScale(math::vec3::cref)
Получение и установка масштаба в мировых или локальных координатах
math::vec3 getWorldUp() const

math::vec3 getWorldForward() const

math::vec3 getWorldRight() const
Получение нормализованных векторов в направлениях вверх, вперёд и вправо пространства модели в мировых координатах
math::Transform::cref getWorldTransform()     const

void setWorldTransform(math::Transform::cref)

math::Transform::cref getLocalTransform() const

void setLocalTransform(math::Transform::cref)
Получение и установка трансформации в мировых или локальных координатах. Трансформация - сочетание позиции, поворота и масштаба
math::Rotator::cref getWorldRotation() const

void setWorldRotation(math::Rotator::cref)

math::Rotator::cref getLocalRotation() const

void setLocalRotation(math::Rotator::cref)

void rotateLocal(const math::Rotator&rotator)

void rotateWorld(const math::Rotator&rotator)
Получение и установка поворота в мировых или локальных координатах. Методы rotateWorld/Local добавляют поворот к текущим значениям
const std::unordered_set<w4::sptr<Node>>& getChildren() const
Получение списка “детей“
bool isEnabled() const
Возвращает, включен ли узел
bool hasParent() const
Возвращает имеет ли узел “родителя“

Hierarchy Example

Возьмём реальные астрономические цифры и построим упрощённую модель солнечной системы. Так как используются космические расстояния, обозреть всю систему не представляется возможным (хотя ничего не мешает попробовать сделать модель более наглядной). В данном случае, модель взята не для визуализации, а для примера возможностей иерархии и вложенности трансформов.

#include "W4Framework.h"

W4_USE_UNSTRICT_INTERFACE

class Planet
{
public:
//Немного данных о небесном теле:
// - имя
// - цвет
// - радиус (км)
// - радиус орбиты (км)
// - период оборота вокруг главного тела (дней)
// - угол отклонения орбиты относительно плоскости обращения вокруг главного тела
// - наклон оси
// - список спутников
 Planet(const std::string& name, const color& planetColor, float planetRadius, float planetOrbit, float siderealPeriod, float eclipseAngle, float axisTilt, const std::vector<Planet>& moons)
    : m_color(planetColor),
      m_radius(planetRadius),
      m_orbit(planetOrbit),
      m_siderealPeriod(siderealPeriod),
      m_eclipseAngle(eclipseAngle),
      m_axisTilt(axisTilt),
      m_moons(moons)
 {}
 color m_color;
 float m_radius;
 float m_orbit;
 float m_siderealPeriod;
 float m_eclipseAngle;
 float m_axisTilt;
 std::vector<Planet> m_moons;
};

struct SolarDemo : public IGame
{
public:
    void onStart() override
    {
        //на старте создаём солнечную систему
        createSolarSystem();
    }

    void onMove(const event::Touch::Move& evt)
    {
    }

    void onUpdate(float dt) override
    {
        for(auto& rotation: m_rotations)
        {
            //вращаем тела вокруг их главных тел со скоростью 0.5 дня в секунду
            rotation.first->rotateLocal(rotation.second * (dt / 2));
        }
    }

private:

    void createSolarSystem()
    {
        // вот она - Солнечная система!
        const Planet solarSystem("Sun", color::yellow, 695500.f, 0.f, 345.39f, 0.f, 0.f, {
            {"Mercury", color::white,                        2439.7f,   57909050.f, .241f,   7.01f,    .0352f, {}},
            {"Venus",   color::magenta,                      6051.8f,  108208000.f, .615,    3.39f, 177.36f,   {}},
            {"Earth",   color::green,                        6378.f,   149598261.f, 1.f,     0.f,    23.44f,   {
                        {"Moon", color::gray, 1737.1f,  384399.f, 1.f, 0.f, 0.f, {}}
                }},
            {"Mars",    color::red,                          3393.5f,  227939100.f, 1.88f,   1.85f,  25.19f,   {
                        {"Phobos", color::gray, 11.2667f, 9377.2f, .317f,   1.093f, 0.f, {}},
                        {"Demos",  color::gray, 10.f,   23458.f, 1.2625f,   1.85f , 0.f, {}}
                }},
            {"Jupiter", color(1.f, .5f, 0.f, 1.f),          71400.f,   778547200.f, 11.86f,  1.31f,   3.13f,   {
                        {"Callisto", color::gray, 2410.3f, 1882709.f, 16.69f, .205f, 0.f, {}},
                        {"Europa",   color::gray, 1560.8f,  671034.f, 3.55f, .471f, 0.f,  {}},
                        {"Ganymede", color::gray, 2634.1f, 1070412.f, 7.15f, .204f, 0.f,  {}},
                        {"Io",       color::gray, 1818.f,   421700.f, 1.77f, .05f, 0.f,   {}}
                }},
            {"Saturn",  (color::yellow + color::white) / 2, 60000.f,  1433449370.f, 29.46f,  2.49f,  26.73f,   {
                        {"Dione",     color::gray, 560.f,   377415.f, 2.74f,      .028f, 0.f, {}},
                        {"Enceladus", color::gray, 251.4f,  238042.f, 1.370218f, 0.f, 0.f,    {}},
                        {"Tethys",    color::gray, 530.f,   294672.f, 1.887802f, 1.091f, 0.f, {}},
                        {"Titan",     color::gray, 2575.f, 1221865.f, 15.945f,    .306f, 0.f, {}}
                }},
            {"Uranus",  color::cyan,                        25600.f,  2876679082.f, 84.01f,  .77f,   97.77f,   {
                        {"Ariel",   color::gray, 578.9f,  191020.f, 2.520379f,  .26f,  0.f, {}},
                        {"Oberon",  color::gray, 761.4f,  583520.f, 13.463239f, .058f, 0.f, {}},
                        {"Titania", color::gray, 588.9f,  435910.f, 8.705872f,  .34f , 0.f, {}},
                        {"Umbriel", color::gray, 584.7f,  266300.f, 4.144177f,  .205f, 0.f, {}}
                }},
            {"Neptune", color::blue,                        24300.f,  4503443661.f, 164.79f, 1.77f,  28.32f,   {
                        {"Triton", color::gray, 1353.4, 4503443661.f, 164.79f, 1.77f, 0.f, {}}
                }}
        });

        //Создаём Солнце
        auto sun = Mesh::create::sphere(solarSystem.m_radius, 10, 10);
        //Добавляем его в корневой узел
        Render::getRoot()->addChild(sun);

        //Устанавливаем цвет для базового материала
        sun->getMaterialInst()->setParam("baseColor", solarSystem.m_color);
        //Создаём планеты
        createMoons(sun, solarSystem);
        //Устанавливаем масштаб. Увы, расстояния такие, что с реальными расстояниями без лупы видно только Солнце... Но это пример.
        sun->setLocalScale({1.e-7f, 1.e-7f, 1.e-7f});
    }
    void createMoons(cref<Node> planetNode, const Planet& planet)
    {
        //для каждого спутника
        for(const auto& moon: planet.m_moons)
        {
            //для иллюстрации вложенности - создадим узел - орбиту, при вращении которого вокруг центра тело будет передвигаться по орбите
            auto orbitContainer = make::sptr<Node>();
            //отклоняем орбиту от плоскости вращения главного тела
            orbitContainer->setLocalRotation({(planet.m_axisTilt + moon.m_eclipseAngle) * DEG2RAD, 0, 0});
            //создаём тело
            auto moonNode = Mesh::create::sphere(moon.m_radius, 10, 10);
            //добавляем спутник как "ребёнка" орбиты
            orbitContainer->addChild(moonNode);
            //устанавливаем позицию телав на орбите
            moonNode->setLocalTranslation({moon.m_orbit, 0, 0});
            moonNode->getMaterialInst()->setParam("baseColor", moon.m_color);
            //добавляем орбиту как "ребёнка" главного тела. Особое внимание на параметр false - он обозначает, 
            //что при чайлдинге сохраняются локальные трансформы. Таким образом, орбита окажется в той же позиции, 
            //что и планета и наклонена относительно его плоскости обращения
            planetNode->addChild(orbitContainer, false);
            //рекурсивно создаём спутники
            createMoons(moonNode, moon);

            //добавляем в список орбиты и кватернионы для их поворота в локальном пространстве (вычисляем из углов эйлера)
            m_rotations.emplace_back(orbitContainer, Rotator(0, 0, 1 / moon.m_siderealPeriod));
        }
    }

    std::vector<std::pair<sptr<Node>, Rotator>> m_rotations;
};

W4_RUN(SolarDemo)

VisibleNode

Базовый класс для отображаемого узла.

Особенности:

  • Все отображаемые узлы содержат вертексный буфер и состоят из одного и более элемента surface (который содержит индексный буфер).
  • Каждому surface можно назначить свой материал.

Все отображаемые узлы имеют следующие дополнительные методы:

Метод Описание
const MaterialInstPtr& getMaterialInst(const std::string& surfaceName) const
Возвращает материал указанного surface
const MaterialInstPtr& getMaterialInst() const
Получает материал, установленный для всех surface. Если материалы surface отличаются - выдаёт ошибку в лог и возвращает какой-то материал.
void setMaterialInst(const MaterialInstPtr & m)
Устанавливает материал для всех surface

Mesh

Объект, отображающий геометрию.

Объекты типа Mesh в движке можно создать тремя путями: 1. Загрузить из ресурсов (см. также Mesh Converter):

teapot = Asset::load(Path("resources/teapot", "teapot.asset"))->getRoot()->getChild<Mesh>("Utah Teapot Quads");

2. Вызвать встроенный генератор:

auto sphere = Mesh::create::sphere(радиус, количество параллелей, количество меридианов);
auto box = Mesh::create::box({ширина, высота, глубина}); //box - параллелепипед, у которого каждая грань - surface
auto cube = Mesh::create::cube({ширина, высота, глубина}); 
auto mappedCube = Mesh::create::mappedCube({ширина, высота, глубина}); //параллелепипед с uv координатами под развёртку
auto plane1 = Mesh::create::plane({ширина, высота}, scaleUv); //плоскость по осям XY. scaleUv = false(по умолчанию) - UV от 0 до 1, true - от 0 до ширины/высоты
auto plane2 = Mesh::create::plane({лево, низ}, {право, верх}, scaleUv; //то же самое
auto cylinder = Mesh::create::cylinder(высота, радиус, количество секторов);
auto cone = Mesh::create::cone(верхний радиус, нижний радиус, высота, количество секторов, количество сегментов по вертикали);
auto torus = Mesh::create::torus(радиус кольца, радиус "трубы", количество сегментов кольца, количество сегментов "трубы", угол дуги кольца(2*PI для замкнутого тора));
auto skybox = Mesh::create::skybox(); //Скайбокс. Единичный куб со сторонами, отображаемыми изнутри.

3. Создать из кода:

        auto indicesBuf = make::sptr<UserIndicesBuffer>("Floor indices");
        auto verticesBuf = make::sptr<UserVerticesBuffer<Mesh::VertexFormat>>("floor_" + std::to_string(width) + "x" + std::to_string(height) + "-" + std::to_string(offset));
        indicesBuf->setNeedExport(false);
        verticesBuf->setNeedExport(false);

        resources::MeshVertexDataBuilder vdb(verticesBuf, indicesBuf);
        //позиция, UV, нормаль
        vdb.addVertex({-width / 2 + offset, 0, height / 2}, {offset/width, 0.f}, {0, 1, 0});
        vdb.addVertex({width / 2 - offset, 0, height / 2}, {1.f - offset/width, 0.f}, {0, 1, 0});
        vdb.addVertex({-width / 2, 0, height / 2 - offset}, {0.f, offset/height}, {0, 1, 0});
        vdb.addVertex({width / 2, 0, height / 2 - offset}, {1.f, offset/height}, {0, 1, 0});

        vdb.addVertex({width / 2, 0, -height / 2 + offset}, {0.f, 1.f - offset/height}, {0, 1, 0});
        vdb.addVertex({-width / 2, 0, -height / 2 + offset}, {1.f, 1.f - offset/height}, {0, 1, 0});
        vdb.addVertex({width / 2 - offset, 0, -height / 2}, {offset/width, 1.f}, {0, 1, 0});
        vdb.addVertex({-width / 2 + offset, 0, -height / 2}, {1.f - offset/width, 1.f}, {0, 1, 0});

        vdb.addIndices({2, 1, 0,
                                2, 3, 1,
                                4, 3 ,2,
                                5, 4, 2,
                                5, 6, 4,
                                7, 6, 5});

        vdb.build();

        auto result = make::sptr<Mesh>(verticesBuf->getName(), verticesBuf);
        result->addComponent<Surface>("unnamed", indicesBuf);

        result->setMaterialInst(Material::getDefault()->createInstance());

SkinnedMesh

Объект, отображающий геометрию с анимацией.

Особенности:

  • можно загрузить только из файла;
  • представляет собой скелетную анимацию;
  • содержит в себе API для управления анимациями и позволяет проигрывать несколько анимаций с плавными переходами между ними.
Метод Описание
w4::cref<core::Node> createSocket(const std::string& boneName)
Создаёт точку крепления на кости с заданным именем. К этому узлу можно добавлять “детей“.
w4::cref<core::Node> getSocket(const std::string& boneName) const
Возвращает точку крепления на кости с заданным именем
std::vector<std::string> getAvailableAnimations() const
Возвращает список полученных анимация
bool haveAnimation(const std::string& animationName) const
Проверяет наличие анимации с заданным именем
size_t getAnimationIndex(const std::string& animationName) const
Получение индекса анимации по её имени.
const std::string& getAnimationName(size_t animationIndex) const
Получение имени анимации по её индексу.
void setAnimationStateChangedCallback(std::function<void(Animator&, Animator::State)>)

void setAnimationStateChangedCallback(Animator::State, std::function<void(Animator&)>)
Установить функтор, который вызовется по изменению состояния конкретной анимации. Метод имеет как вариант только для заданного состояния, так и для любого. Состояния анимаций:
Animator::State::Idle(не проигрывается/закончилась), 
Animator::State::Play(проигрывается), 
Animator::State::Pause(приостановлена)

Пример:

node->setAnimationStateChangedCallback(Animator::State::Idle, [](Animator& animator)
{
    W4_LOG_DEBUG("Animator stopped:\n"
                 "\tName: %s,"
                 "\tSpeed: %f,"
                 "\tDuration: %f,"
                 "\tFps: %f",
                    animator.getName().c_str(),
                    animator.getSpeed(),
                    animator.getDuration(),
                    animator.getFps());
});
void play(const std::string& animationName, float duration = 0.f)

void play(size_t animationIndex = 0, float duration = 0.f)
Проигрывание анимации по индексу или имени. Указывается длительность анимации, если она равна нулю - проигрывается анимация целиком.
void play(std::initializer_list<std::pair<std::string, float>>, float duration = 0.f)

void play(std::initializer_list<std::pair<size_t, float>>, float duration = 0.f)
Проигрывание списка анимаций с весами. Длительность, если равна нулю - равна анимации с максимальной длительностью.

Пример:

skinned->play({{"run", .3f}, {"jump", .7f}});
void pause()
Приостановить все активные анимации.
void resume()
Возобновить проигрывание всех приостановленных анимаций.
void stop()
Остановить проигрывание всех активных анимаций.
bool isPlaying() const
Проверяет, проигрывается ли сейчас любая анимация.
Animator & getAnimator(size_t)
Animator & getAnimator(const std::string&)
const Animator & getAnimator(size_t) const
const Animator & getAnimator(const std::string&) const
Получить объект аниматор по имени или индексу. API объекта Animator описан чуть ниже. Используется для управления анимациями с расширенными возможностями.

Управление анимациями через API и SkinnedMesh не являются взаимоисключающими. Так, например, общеупотребительной является практика зацикливания анимаций:

m_skinned->getAnimator("dance").setIsLooped(true);
Animator API
Метод Описание
xxxxxxxxxx
Example
xxxxxxxxxx
Example
xxxxxxxxxx
Example
xxxxxxxxxx
Example
xxxxxxxxxx
Example
xxxxxxxxxx
Example
xxxxxxxxxx
Example

ParticlesEmitter

Billboard

Plotter

DataNode

ArcBallNode

Camera

PointLight

SpotLight

Spline