vertecs
Version:
A typescript entity-component-system framework
409 lines (349 loc) • 11.3 kB
text/typescript
/**
* A transform represents a position, rotation and a scale, it may have a parent Transform,
*
* Global position, rotation and scale are only updated when dirty and queried,
* parents are updated from the current transform up to the root transform.
*/
import {
mat4,
Mat4,
Mat4Like,
Quat,
QuatLike,
Vec3,
Vec3Like,
} from "ts-gl-matrix";
import { Component, Entity } from "../core";
export default class Transform extends Component {
/**
* The result of the post-multiplication of all the parents of this transform
* This matrix is only updated when queried and dirty
* @private
*/
readonly modelToWorldMatrix: Mat4;
/**
* The inverse of {@see modelToWorldMatrix}
* This matrix is only updated when queried and dirty
* @private
*/
readonly worldToModelMatrix: Mat4;
/**
* The result of Translation * Rotation * Scale
* This matrix is only updated when queried and dirty
* @private
*/
readonly modelMatrix: Mat4;
/**
* The current position
* @private
*/
readonly position: Vec3;
/**
* The current rotation
* @private
*/
readonly rotation: Quat;
/**
* The current scale
* @private
*/
readonly scaling: Vec3;
dirty: boolean;
forward: Vec3;
/**
* The parent transform
* @private
*/
parent?: Transform;
#worldPosition: Vec3;
#worldRotation: Quat;
#worldScale: Vec3;
/**
* Creates a new transform
* @param translation Specifies the translation, will be copied using {@link Vec3.copy}
* @param rotation Specifies the rotation, will be copied using {@link Quat.copy}
* @param scaling Specifies the scale, will be copied using {@link Vec3.copy}
* @param forward Specifies the forward vector, will be copied using {@link Vec3.copy}
*/
public constructor(
translation?: Vec3Like,
rotation?: QuatLike,
scaling?: Vec3Like,
forward?: Vec3
) {
super();
this.modelMatrix = Mat4.create();
this.modelToWorldMatrix = Mat4.create();
this.worldToModelMatrix = Mat4.create();
this.position = Vec3.create();
this.rotation = Quat.create();
this.scaling = Vec3.fromValues(1, 1, 1);
this.forward = forward ?? Vec3.fromValues(0, 0, 1);
this.#worldPosition = Vec3.create();
this.#worldRotation = Quat.create();
this.#worldScale = Vec3.create();
if (translation) Vec3.copy(this.position, translation);
if (rotation) Quat.copy(this.rotation, rotation);
if (scaling) Vec3.copy(this.scaling, scaling);
this.dirty = true;
}
/**
* Set this transform's parent to the entity's parent
* @param entity The entity this transform is attached to
*/
public onAddedToEntity(entity: Entity) {
if (entity.parent) {
this.setNewParent(entity.parent);
}
}
/**
* Remove this transform's parent
* @param entity The entity this transform was attached to
*/
public onRemovedFromEntity(entity: Entity): void {
this.parent = undefined;
}
/**
* Called whenever the attached entity parent change
* @param parent The new parent entity
*/
public onEntityNewParent(parent: Entity): void {
this.setNewParent(parent);
}
private setNewParent(parent: Entity): void {
if (!parent.getComponent(Transform)) {
parent.addComponent(new Transform());
}
this.parent = parent.getComponent(Transform);
this.dirty = true;
}
public onDestroyed(): void {}
public static fromMat4(matrix: Mat4): Transform {
const transform = new Transform();
transform.copy(matrix);
return transform;
}
/**
* Copy the translation, rotation and scaling of the transform
* @param transform The transform to copy from
*/
public copy(transform: Mat4) {
Mat4.getTranslation(this.position, transform);
Mat4.getRotation(this.rotation, transform);
Mat4.getScaling(this.scaling, transform);
this.dirty = true;
}
/**
* Translate this transform using a translation vector
* @param translation The translation vector
*/
public translate(translation: Vec3Like): void {
Vec3.add(this.position, this.position, translation);
this.dirty = true;
}
public setWorldRotation(rotation: QuatLike): void {
const inverseWorldRotation = Quat.invert(
Quat.create(),
this.getWorldRotation()
);
Quat.multiply(this.rotation, this.rotation, inverseWorldRotation);
Quat.mul(this.rotation, this.rotation, rotation);
this.dirty = true;
}
/**
* Reset the position, rotation, and scale to the default values
*/
public reset(): void {
Vec3.set(this.position, 0, 0, 0);
this.resetRotation();
Vec3.set(this.scaling, 1, 1, 1);
this.dirty = true;
}
/**
* Reset the rotation to the default values
*/
public resetRotation(): void {
Quat.set(this.rotation, 0, 0, 0, 1);
this.dirty = true;
}
/**
* Rotate this transform in the x axis
* @param x An angle in radians
*/
public rotateX(x: number): void {
Quat.rotateX(this.rotation, this.rotation, x);
this.dirty = true;
}
/**
* Rotate this transform in the y axis
* @param y An angle in radians
*/
public rotateY(y: number): void {
Quat.rotateY(this.rotation, this.rotation, y);
this.dirty = true;
}
/**
* Rotate this transform in the y axis
* @param z An angle in radians
*/
public rotateZ(z: number): void {
Quat.rotateZ(this.rotation, this.rotation, z);
this.dirty = true;
}
public rotate(rotation: QuatLike): void {
Quat.mul(this.rotation, this.rotation, rotation);
this.dirty = true;
}
public setWorldScale(scale: Vec3Like): void {
const inverseScale = Vec3.inverse(Vec3.create(), this.getWorldScale());
Vec3.mul(this.scaling, this.scaling, inverseScale);
Vec3.mul(this.scaling, this.scaling, scale);
this.dirty = true;
}
public scale(scale: Vec3Like): void {
Vec3.multiply(this.scaling, this.scaling, scale);
this.dirty = true;
}
public setScale(scale: Vec3Like) {
Vec3.copy(this.scaling, scale);
this.dirty = true;
}
public setRotationQuat(rotation: QuatLike): void {
Quat.copy(this.rotation, rotation);
this.dirty = true;
}
/**
* Updates the model to world matrix of this transform and returns it
* It update all the parents until no one is dirty
*/
public updateModelToWorldMatrix(): Mat4 {
if (!this.dirty && !this.parent?.dirty) {
return this.modelToWorldMatrix;
}
// Update the model matrix
Mat4.fromRotationTranslationScale(
this.modelMatrix,
this.rotation,
this.position,
this.scaling
);
// If the object has a parent, multiply its matrix with the parent's
if (this.parent) {
const parentMatrix = this.parent.updateModelToWorldMatrix();
Mat4.mul(this.modelToWorldMatrix, parentMatrix, this.modelMatrix);
} else {
Mat4.copy(this.modelToWorldMatrix, this.modelMatrix);
}
Mat4.getTranslation(this.#worldPosition, this.modelToWorldMatrix);
Mat4.getRotation(this.#worldRotation, this.modelToWorldMatrix);
Mat4.getScaling(this.#worldScale, this.modelToWorldMatrix);
return this.modelToWorldMatrix;
}
public getWorldToModelMatrix(): Mat4Like {
return Mat4.invert(
this.worldToModelMatrix,
this.updateModelToWorldMatrix()
);
}
public getWorldPosition(): Vec3 {
if (this.dirty) {
this.updateModelToWorldMatrix();
}
return this.#worldPosition;
}
/**
* Get the world scale of this transform
*/
public getWorldScale(): Vec3 {
if (this.dirty) {
this.updateModelToWorldMatrix();
}
return this.#worldScale;
}
/**
* Get the world rotation of this transform
*/
public getWorldRotation(): Quat {
if (this.dirty) {
this.updateModelToWorldMatrix();
}
return this.#worldRotation;
}
public getWorldForwardVector(out: Vec3): Vec3 {
const worldRotation = this.getWorldRotation();
Vec3.normalize(
out,
Vec3.transformQuat(Vec3.create(), this.forward, worldRotation)
);
return out;
}
public getVectorInModelSpace(out: Vec3, vector: Vec3Like): Vec3 {
this.updateModelToWorldMatrix();
Mat4.invert(this.worldToModelMatrix, this.modelToWorldMatrix);
Vec3.transformMat4(out, vector, this.worldToModelMatrix);
return out;
}
public getVectorInWorldSpace(out: Vec3, vector: Vec3): Vec3 {
Vec3.transformMat4(out, vector, this.updateModelToWorldMatrix());
return out;
}
public toWorldScale(out: Vec3, scale: Vec3Like): Vec3 {
Vec3.mul(out, scale, Vec3.inverse(Vec3.create(), this.getWorldScale()));
return out;
}
/**
* Make this transform look at the specified position
* @param x
* @param y
* @param z
*/
public lookAtXyz(x: number, y: number, z: number): void {
const lookAtMatrix = Mat4.create();
mat4.targetTo(
lookAtMatrix,
[x, y, z],
this.getWorldPosition(),
[0, 1, 0]
);
Mat4.getRotation(this.rotation, lookAtMatrix);
this.dirty = true;
}
public lookAt(position: Vec3): void {
this.lookAtXyz(position[0], position[1], position[2]);
}
public setWorldUnitScale(): void {
const modelToWorldMatrix = this.updateModelToWorldMatrix();
const scale = Vec3.create(); // TODO: Cache this
const currentScale = Vec3.create(); // TODO: Cache this
Vec3.div(
scale,
[1, 1, 1],
Mat4.getScaling(currentScale, modelToWorldMatrix)
);
this.scale(scale);
}
public setWorldPosition(position: Vec3Like): void {
const worldPosition = this.getWorldPosition();
Vec3.sub(position, position, worldPosition);
Vec3.add(this.position, this.position, position);
this.dirty = true;
}
/**
* Set the current position
* @param position Specifies the new position, will be copied using {@link Vec3.copy}
*/
public setPosition(position: Vec3Like) {
Vec3.copy(this.position, position);
this.dirty = true;
}
public setForward(forward: Vec3Like): void {
Vec3.copy(this.forward, forward);
this.dirty = true;
}
/**
* Return a new Transform with the same position, rotation, scaling, but no parent
*/
public clone(): Component {
return new Transform(this.position, this.rotation, this.scaling);
}
}