Component: Transform
Coordinate Systemâ
In the Arche project, the right-handed coordinate system is used uniformly, and to maintain uniformity with the
matrix used in the shader, both the matrix and the vector are stored in column-major. In addition, in order to make a more
detailed distinction between some operations in terms of mathematical concepts, we distinguish between Vector
and Point
. These two types are very similar. For example, the length of a vector can be calculated, and the
length from a point to the origin can also be calculated. Point
can add a displacement represented by a Vector
to
get a new Point
, and two Point
Adding only gives a Vector
representing the displacement. Therefore, it is
necessary to pay attention to distinguish these two concepts in specific use.
tip
C++ Templates are used for all math types, float
is generally used for rendering, and double
is a common choice
for simulation projects. Using template types, you can let the programmer choose the precision as needed, and each type
provides castTo
member functions to facilitate mutual conversion. As a result, simulated programs can be easily visualized in
conjunction with rendering component without introducing another set of math classes.
caution
There is no distinction between Point
and Vector
in Arche.js, mainly because JavaScript does not have
operator overloading, which makes it easy to confuse the use of too many mathematical concepts, so a more streamlined
design is adopted.
Transform componentâ
Transform component can be said to be the most basic component, which package these concepts into a description
of Entity
based on Point
, Vector
, Quaternion
and Matrix4x4
. And use the pointer of the child and parent entity
saved in the Entity
to get the coordinates and transformation matrix in world space.
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 marksâ
In the specific implementation, since more information needs to be updated each time, including position, scaling, rotation, etc., dirty marks are introduced:
/**
* 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
};
When a variable is modified, it will not only modify the value of the current Transform
, but also record the dirty
mark on the corresponding Transform
components of all child entities:
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();
}
}
}
In this way, when the pose information of the Transform
component corresponding to the child entity is obtained,
the _getParentTransform
member function will be triggered to update the data from the parent entity because it is
marked as dirty:
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;
}
Update notificationâ
In addition to marking in the associated Transform
so that when a component is updated, related components can also be
updated synchronously, there is also an observer pattern to be notified of data updates. This mechanism is implemented
through UpdateFlagManager
. For other objects that need to be observed, you can call:
std::unique_ptr<UpdateFlag> Transform::registerWorldChangeFlag() {
return _updateFlagManager.registration();
}
_worldAssociatedChange
is fired when Transform
changes, which causes all UpdateFlag
registered
with UpdateFlagManager
to be flagged:
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;
}
}
Therefore, you only need to observe the state of UpdateFlag
to know whether the corresponding state needs to be
updated. For example, in the physics component, Collider
synchronizes the poses of Entity
and PxRigidActor
in this
way:
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);
}
}
}