跳到主要内容

组件:变换

坐标系

在 Arche 项目中,统一使用右手坐标系,且为了与着色器中使用的矩阵保持统一,矩阵和向量均采用列主元。 除此之外,为了在数学概念上对一些运算做更加细致的区分,因此区分了向量 Vector 和点 Point 这两个概念。 这两个类型非常相似,例如可以计算向量的长度,也可以计算点到原点的长度,Point 可以加上一段 Vector 表示的位移得到一个新的 Point, 而两个 Point 相加只能得到表示位移的 Vector。 因此,在具体使用时需要注意区分这两个概念。

tip

所有数学类型都使用了 C++ 模板,对于渲染来说,一般使用 float, 而对于模拟项目来说 double 是一个常见的选择。使用模板类型,可以让程序按需选择精度,并且每一类型都提供了 castTo 成员函数,方便相互转换。由此,模拟的程序很容易结合渲染组件进行可视化,而无需额外引入另外一套数学类。

caution

在 Arche.js 中并未做 PointVector 的区分,主要是因为 JavaScript 不存在运算符重载,使得定义过多的数学概念很容易在使用时产生混淆,因此采用了更加精简的设计。

变换组件

变换组件可以说是最为基础的组件,其在Point, Vector, QuaternionMatrix4x4的基础上将这些概念打包成对于 Entity 的描述,并且通过Entity 中保存的子父实体的指针,得到世界空间中的坐标和变换矩阵。

class Transform : public Component {
public:
Transform(Entity *entity);

/**
* Local position.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
Point3F position();

void setPosition(const Point3F &value);

/**
* World position.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
Point3F worldPosition();

void setWorldPosition(const Point3F &value);

...
};

脏标记

在具体实现中,由于每次需要更新的信息较多,包括位置,缩放,旋转等等,因此引入了脏标记:

/**
* Dirty flag of transform.
*/
enum TransformFlag {
LocalEuler = 0x1,
LocalQuat = 0x2,
WorldPosition = 0x4,
WorldEuler = 0x8,
WorldQuat = 0x10,
WorldScale = 0x20,
LocalMatrix = 0x40,
WorldMatrix = 0x80,

/** WorldMatrix | WorldPosition */
WmWp = 0x84,
/** WorldMatrix | WorldEuler | WorldQuat */
WmWeWq = 0x98,
/** WorldMatrix | WorldPosition | WorldEuler | WorldQuat */
WmWpWeWq = 0x9c,
/** WorldMatrix | WorldScale */
WmWs = 0xa0,
/** WorldMatrix | WorldPosition | WorldScale */
WmWpWs = 0xa4,
/** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale */
WmWpWeWqWs = 0xbc
};

当修改了某一变量时,不仅会修改当前 Transform 的值,而且会将脏标记记录到所有子实体对应的 Transform 组件上:

void Transform::setPosition(const Point3F &value) {
_position = value;
_setDirtyFlagTrue(TransformFlag::LocalMatrix);
_updateWorldPositionFlag();
}

void Transform::_updateWorldPositionFlag() {
if (!_isContainDirtyFlags(TransformFlag::WmWp)) {
_worldAssociatedChange(TransformFlag::WmWp);
const auto &nodeChildren = _entity->_children;
for (size_t i = 0, n = nodeChildren.size(); i < n; i++) {
nodeChildren[i]->transform->_updateWorldPositionFlag();
}
}
}

这样一来,当获取子实体对应的 Transform 组件的姿态信息时,由于被标记了脏标记,就会触发 _getParentTransform 成员函数,从父实体中更新数据:

Point3F Transform::worldPosition() {
if (_isContainDirtyFlag(TransformFlag::WorldPosition)) {
if (_getParentTransform()) {
_worldPosition = getTranslation(worldMatrix());
} else {
_worldPosition = _position;
}
_setDirtyFlagFalse(TransformFlag::WorldPosition);
}
return _worldPosition;
}

Transform *Transform::_getParentTransform() {
if (!_isParentDirty) {
return _parentTransformCache;
}
Transform *parentCache = nullptr;
auto parent = _entity->parent();
while (parent) {
const auto &transform = parent->transform;
if (transform) {
parentCache = transform;
break;
} else {
parent = parent->parent();
}
}
_parentTransformCache = parentCache;
_isParentDirty = false;
return parentCache;
}

更新通知

除了在关联的 Transform 中标记,使得更新一个组件,相关组件也可以同步更新外,还存在一种观察者模式来获得数据更新的通知。这种机制是通过 UpdateFlagManager 来实现的。 对于需要观察的其他对象,都可以调用:

std::unique_ptr<UpdateFlag> Transform::registerWorldChangeFlag() {
return _updateFlagManager.registration();
}

Transform 发生改变时,_worldAssociatedChange 会被触发,这使得通过 UpdateFlagManager 注册的所有 UpdateFlag 都会被标记:

void Transform::_worldAssociatedChange(int type) {
_dirtyFlag |= type;
_updateFlagManager.distribute();
}

void UpdateFlagManager::distribute() {
for (size_t i = 0; i < _updateFlags.size(); i++) {
_updateFlags[i]->flag = true;
}
}

因此,只需要观察 UpdateFlag 的状态,就能够知道是否需要更新对应的状态。例如,在物理组件当中,Collider 就是通过这一方式,同步 EntityPxRigidActor 的姿态:

void Collider::_onUpdate() {
if (_updateFlag->flag) {
const auto &transform = entity()->transform;
const auto &p = transform->worldPosition();
auto q = transform->worldRotationQuaternion();
q.normalize();
_nativeActor->setGlobalPose(PxTransform(PxVec3(p.x, p.y, p.z), PxQuat(q.x, q.y, q.z, q.w)));
_updateFlag->flag = false;

const auto worldScale = transform->lossyWorldScale();
for (auto &_shape: _shapes) {
_shape->setWorldScale(worldScale);
}
}
}