Basic Concept
Contents
Scope
В статье описаны основные решения, используемые в W4 Game Engine, и их API.
Coordinate System
Используется left-handed координатная система и вращение производится против часовой стрелки, если смотреть в направлении оси.
Render Tree
Отображаемый на экране результат и зависимость трансформаций объектов друг от друга определяются деревом рендеринга, то есть набором объектов рендеринга. Особенности дерева рендеринга W4 Game Engine:
- Минимальное дерево рендеринга состоит только из корневого узла.
- У каждого узла может быть ноль или более подузлов — «детей», но при этом у узла не может быть более одного “родителя“ (самый верхний узел - “корневой“ - родителя не имеет).
- Для того чтобы объект отобразился на экране и/или пересчитал свои данные, необходимо чтобы он находился в дереве.
- Иерархия встроенных типов узлов показана на схеме.
Примечания по использованию:
- При рисовании в несколько проходов, у каждого прохода создаётся своё дерево со своим корневым узлом.
- У каждого узла можно узнать локальные трансформации (то есть трансформации относительно “родителя“) и мировые, а также задать их.
- При задании мировых координат узла пересчитываются его локальные координаты, при задании локальных - мировые.
- У узла, не имеющего родителя трансформации совпадают.
- Можно временно отключить узел, при этом отключаются и все его “потомки“.
Интерфейсы узлов (классов) описаны далее.
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}});
|
xxxxxxxxxx
|
Example |
xxxxxxxxxx
|
Example |
xxxxxxxxxx
|
Example |
xxxxxxxxxx
|
Example |