Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add globalPosition getter/setter to Transform #1032

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"--async-stack-traces"
],
"args": [
"${fileBasename}",
"${file}",
"--verbose",
"--no-cache",
"-i"
Expand Down
109 changes: 109 additions & 0 deletions packages/@dcl/ecs/src/components/extended/Transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { DeepReadonly, IEngine, LastWriteWinElementSetComponentDefinition } from '../../engine'
import { Entity } from '../../engine/entity'
import { Vector3Type } from '../../schemas'
import { TransformSchema, TransformType } from '../manual/TransformSchema'
import { getGlobalPositionHelper } from './transform-utils/globalPosition'

/**
* @public
*/
export type TransformComponent = LastWriteWinElementSetComponentDefinition<TransformType>

/**
* @public
*/
export type MutableTransform = TransformType & {
// Get calculated global position or set local position by passing the resultant one
// Log and error in case of failing. Cyclic parenting, invalid scales or rotations could raise undefined behavior
// Warning: this property usage might be expensive, use it wisely
globalPosition: Vector3Type
}

/**
* @public
*/
export type ReadonlyTransform = DeepReadonly<TransformType> & {
// Get calculated global position
// Log and error in case of failing. Cyclic parenting, invalid scales or rotations could raise undefined behavior
// Warning: this property usage might be expensive, use it wisely
globalPosition: Readonly<Vector3Type>
}

/**
* @public
*/
export interface TransformComponentExtended extends TransformComponent {
create(entity: Entity, val?: TransformTypeWithOptionals): MutableTransform
createOrReplace(entity: Entity, val?: TransformTypeWithOptionals): MutableTransform
get(entity: Entity): ReadonlyTransform
getOrNull(entity: Entity): ReadonlyTransform | null
getMutable(entity: Entity): MutableTransform
getMutableOrNull(entity: Entity): MutableTransform | null
getOrCreateMutable(entity: Entity, initialValue?: TransformTypeWithOptionals): MutableTransform
}

/**
* @public
*/
export type TransformTypeWithOptionals = Partial<TransformType>

export function defineTransformComponent(
engine: Pick<IEngine, 'defineComponentFromSchema'>
): TransformComponentExtended {
const transformDef = engine.defineComponentFromSchema('core::Transform', TransformSchema)
const definePropertiesIfNotSet = getGlobalPositionHelper(transformDef)

return {
...transformDef,
create(entity: Entity, val?: TransformTypeWithOptionals) {
const value = transformDef.create(entity, TransformSchema.extend!(val))
definePropertiesIfNotSet(value)
return value as MutableTransform
},
createOrReplace(entity: Entity, val?: TransformTypeWithOptionals) {
const value = transformDef.createOrReplace(entity, TransformSchema.extend!(val))
definePropertiesIfNotSet(value)
return value as MutableTransform
},
getOrNull(entity: Entity): ReadonlyTransform | null {
const component = transformDef.__data.get(entity)
if (component) {
definePropertiesIfNotSet(component)
}
return component ? (component as ReadonlyTransform) : null
},

// Functions below depend on the funtiosn above, the functions above are in charge of call `definePropertiesIfNotSet`

getOrCreateMutable(entity: Entity, initialValue?: TransformTypeWithOptionals) {
let value = transformDef.getMutableOrNull(entity)
if (value === null) {
value = this.createOrReplace(entity, initialValue)
} else {
transformDef.__dirtyIterator.add(entity)
}
return value as MutableTransform
},
get(entity: Entity): ReadonlyTransform {
const component = this.getOrNull(entity)
if (component === null) {
throw new Error(`[getFrom] Component ${transformDef.componentName} for entity #${entity} not found`)
}
return component as ReadonlyTransform
},
getMutable(entity: Entity): MutableTransform {
const component = this.get(entity)
if (component) {
transformDef.__dirtyIterator.add(entity)
}
return component as MutableTransform
},
getMutableOrNull(entity: Entity): MutableTransform {
const component = this.getOrNull(entity)
if (component) {
transformDef.__dirtyIterator.add(entity)
}
return component as MutableTransform
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { LastWriteWinElementSetComponentDefinition } from '../..'
import { Entity } from '../../../engine'
import { Vector3Type } from '../../../schemas'
import { Vector3 } from '../../generated/pb/decentraland/common/vectors.gen'
import { TransformType } from '../../manual/TransformSchema'
import { MutableTransform } from '../Transform'
import { applyQuaternionToVector3, multiplyQuaternionToRef, vector3AddToRef, vector3MultiplyToRef } from './math'

export function getGlobalPositionHelper(transformDef: LastWriteWinElementSetComponentDefinition<TransformType>) {
const helperArray: Entity[] = []
function getGlobalTransform(transform: TransformType): TransformType {
const position = { ...transform.position }
const scale = { ...transform.scale }
const rotation = { ...transform.rotation }

helperArray.length = 0
let currentTransform = null
if (transform.parent !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not going to work for network entities.

We should include some logic here for that, like if the entity is marked as sync, look for the network parentEntity instead of the transform.parent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can take two path for this, as it doesn't work with AvatarAttach nor Billboard neither:
a. We add the disclaimer comment to get noticed when this accessor is approachable or not.
b. We proper implement all the possibilities and get this feature reliable.

In case of a) we can move forward with the current implementation and add the comments

In case of b), my question is, is there any mechanism to get the globalPositon so far? Because what I saw in Utils library, this is not implemented. The current getWorldPosition and getWorldRotation doesn't take into consideration the scale.

currentTransform = transformDef.getMutableOrNull(transform.parent)
helperArray.push(transform.parent)
}
while (currentTransform !== null) {
// calculate position
vector3MultiplyToRef(currentTransform.scale, position)
applyQuaternionToVector3(currentTransform.rotation, position)
vector3AddToRef(currentTransform.position, position)

// calculate rotation
multiplyQuaternionToRef(currentTransform.rotation, rotation)

// calculate scale
vector3MultiplyToRef(currentTransform.scale, scale)

const nextTransform: TransformType | null =
currentTransform.parent !== undefined ? transformDef.getMutableOrNull(currentTransform.parent) : null
if (nextTransform !== null) {
// We early return if we find a cyclic parent
if (helperArray.includes(currentTransform.parent!)) {
// TODO: should we throw an error instead?
console.error(`There is a cyclic parent with entity ${currentTransform.parent}`)
return {
position,
scale,
rotation
}
}
helperArray.push(currentTransform.parent!)
}

currentTransform = nextTransform
}

return {
position,
scale,
rotation
}
}

function getLocalPosition(transform: TransformType, globalPosition: Vector3) {
const value = { ...globalPosition }
const parentTransform = transform.parent !== undefined ? transformDef.getMutable(transform.parent) : null
if (parentTransform !== null) {
const parentGlobalTransform = getGlobalTransform(parentTransform)

// Subtract parent position
value.x -= parentGlobalTransform.position.x
value.y -= parentGlobalTransform.position.y
value.z -= parentGlobalTransform.position.z

// Apply inverse rotation of the parent's global rotation
const inverseParentRotation = { ...parentGlobalTransform.rotation }
inverseParentRotation.x *= -1
inverseParentRotation.y *= -1
inverseParentRotation.z *= -1
applyQuaternionToVector3(inverseParentRotation, value)

// Divide by parent scale
value.x /= parentGlobalTransform.scale.x
value.y /= parentGlobalTransform.scale.y
value.z /= parentGlobalTransform.scale.z
}
return value
}

const extendedTransformProperties: PropertyDescriptorMap & ThisType<TransformType> = {
globalPosition: {
get(): Vector3Type {
return getGlobalTransform(this).position
},
set(value: Vector3Type) {
const newPosition = getLocalPosition(this, value)
// if some of its component is NaN, we don't update the position (there is an undefined behaviour otherwise)
if (!Number.isFinite(newPosition.x) || !Number.isFinite(newPosition.y) || !Number.isFinite(newPosition.z)) {
// TODO: should we throw an error instead?
return
}
this.position = newPosition
},
enumerable: false
},
__extended: {
value: true,
enumerable: false,
writable: false
}
}

return (value: Partial<MutableTransform>) => {
if ((value as any).__extended !== true) {
Object.defineProperties(value, extendedTransformProperties)
}
}
}
43 changes: 43 additions & 0 deletions packages/@dcl/ecs/src/components/extended/transform-utils/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Quaternion, Vector3 } from '../../generated/pb/decentraland/common/vectors.gen'

export function multiplyQuaternionToRef(q1: Quaternion, ref: Quaternion): void {
const x = q1.w * ref.x + q1.x * ref.w + q1.y * ref.z - q1.z * ref.y
const y = q1.w * ref.y - q1.x * ref.z + q1.y * ref.w + q1.z * ref.x
const z = q1.w * ref.z + q1.x * ref.y - q1.y * ref.x + q1.z * ref.w
const w = q1.w * ref.w - q1.x * ref.x - q1.y * ref.y - q1.z * ref.z

ref.x = x
ref.y = y
ref.z = z
ref.w = w
}

export function applyQuaternionToVector3(q: Quaternion, ref: Vector3): void {
const { x, y, z } = ref
const qx = q.x,
qy = q.y,
qz = q.z,
qw = q.w

// Calculate quat * vector
const ix = qw * x + qy * z - qz * y
const iy = qw * y + qz * x - qx * z
const iz = qw * z + qx * y - qy * x
const iw = -qx * x - qy * y - qz * z

ref.x = ix * qw + iw * -qx + iy * -qz - iz * -qy
ref.y = iy * qw + iw * -qy + iz * -qx - ix * -qz
ref.z = iz * qw + iw * -qz + ix * -qy - iy * -qx
}

export function vector3AddToRef(v1: Vector3, ref: Vector3): void {
ref.x += v1.x
ref.y += v1.y
ref.z += v1.z
}

export function vector3MultiplyToRef(v1: Vector3, ref: Vector3): void {
ref.x *= v1.x
ref.y *= v1.y
ref.z *= v1.z
}
2 changes: 1 addition & 1 deletion packages/@dcl/ecs/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import defineNameComponent, { NameType } from './manual/Name'
import defineSyncComponent, { ISyncComponentsType } from './manual/SyncComponents'
import defineNetworkEntity, { INetowrkEntityType } from './manual/NetworkEntity'
import defineNetworkParent, { INetowrkParentType } from './manual/NetworkParent'
import { defineTransformComponent, TransformComponentExtended } from './manual/Transform'
import { AudioStreamComponentDefinitionExtended, defineAudioStreamComponent } from './extended/AudioStream'
import { MediaState } from './generated/pb/decentraland/sdk/components/common/media_state.gen'
import { defineVirtualCameraComponent, VirtualCameraComponentDefinitionExtended } from './extended/VirtualCamera'
import { defineTransformComponent, TransformComponentExtended } from './extended/Transform'

export * from './generated/index.gen'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import { LastWriteWinElementSetComponentDefinition, IEngine } from '../../engine'
import { Entity } from '../../engine/entity'
import type { ISchema } from '../../schemas/ISchema'
import { ByteBuffer } from '../../serialization/ByteBuffer'

/**
* @public
*/
export type TransformComponent = LastWriteWinElementSetComponentDefinition<TransformType>

/**
* @public
*/
export interface TransformComponentExtended extends TransformComponent {
create(entity: Entity, val?: TransformTypeWithOptionals): TransformType
createOrReplace(entity: Entity, val?: TransformTypeWithOptionals): TransformType
}

/**
* @internal
*/
Expand Down Expand Up @@ -122,23 +108,3 @@ export const TransformSchema: ISchema<TransformType> = {
serializationType: 'transform'
}
}

/**
* @public
*/
export type TransformTypeWithOptionals = Partial<TransformType>

export function defineTransformComponent(
engine: Pick<IEngine, 'defineComponentFromSchema'>
): TransformComponentExtended {
const transformDef = engine.defineComponentFromSchema('core::Transform', TransformSchema)
return {
...transformDef,
create(entity: Entity, val?: TransformTypeWithOptionals) {
return transformDef.create(entity, TransformSchema.extend!(val))
},
createOrReplace(entity: Entity, val?: TransformTypeWithOptionals) {
return transformDef.createOrReplace(entity, TransformSchema.extend!(val))
}
}
}
7 changes: 6 additions & 1 deletion packages/@dcl/ecs/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ export type { MeshColliderComponentDefinitionExtended } from './extended/MeshCol
export type { TextureHelper, MaterialComponentDefinitionExtended } from './extended/Material'
export type { TweenHelper, TweenComponentDefinitionExtended } from './extended/Tween'
export type { CameraTransitionHelper, VirtualCameraComponentDefinitionExtended } from './extended/VirtualCamera'
export type { TransformComponentExtended, TransformTypeWithOptionals } from './manual/Transform'
export type {
TransformComponentExtended,
TransformTypeWithOptionals,
MutableTransform,
ReadonlyTransform
} from './extended/Transform'
export type { NameComponent, NameType } from './manual/Name'
export type { ISyncComponents, ISyncComponentsType } from './manual/SyncComponents'
export type { INetowrkEntity, INetowrkEntityType } from './manual/NetworkEntity'
Expand Down
2 changes: 1 addition & 1 deletion packages/@dcl/ecs/src/composite/instance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { componentDefinitionByName } from '../components'
import { componentNumberFromName } from '../components/component-number'
import { TransformType } from '../components/manual/Transform'
import { TransformType } from '../components/manual/TransformSchema'
import { Entity } from '../engine/entity'
import { ComponentDefinition, IEngine, LastWriteWinElementSetComponentDefinition } from '../engine/types'
import { Schemas } from '../schemas'
Expand Down
6 changes: 6 additions & 0 deletions packages/@dcl/ecs/src/engine/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export interface BaseComponent<T> {
export interface LastWriteWinElementSetComponentDefinition<T> extends BaseComponent<T> {
readonly componentType: ComponentType.LastWriteWinElementSet

// @internal this is only for internal usage like extended functionality
readonly __data: Map<Entity, T>

// @internal this is only for internal usage like extended functionality
readonly __dirtyIterator: Set<Entity>

// <USER INTERFACE METHODS>
/**
* Get the readonly component of the entity (to mutate it, use getMutable instead),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ export function createComponentDefinitionFromSchema<T>(
for (const cb of cbs) {
cb(value)
}
}
},
__data: data,
__dirtyIterator: dirtyIterator
}
}
3 changes: 2 additions & 1 deletion packages/@dcl/ecs/src/runtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type { SystemFn, SYSTEMS_REGULAR_PRIORITY } from '../engine/systems'
export type { TransportMessage, ReceiveMessage, Transport } from '../systems/crdt/types'
export { TransformType, TransformComponent } from '../components/manual/Transform'
export { TransformType } from '../components/manual/TransformSchema'
export { TransformComponent } from '../components/extended/Transform'
export * from '../engine/component'
export * from '../schemas/typing'
export type { MapResult, Spec } from '../schemas/Map'
2 changes: 1 addition & 1 deletion packages/@dcl/ecs/src/serialization/crdt/network/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { INetowrkEntityType } from '../../../components/types'
import { PutNetworkComponentOperation } from './putComponentNetwork'
import { DeleteComponentNetwork } from './deleteComponentNetwork'
import { DeleteEntityNetwork } from './deleteEntityNetwork'
import { TransformSchema } from '../../../components/manual/Transform'
import { TransformSchema } from '../../../components/manual/TransformSchema'

/* istanbul ignore next */
export function isNetworkMessage(message: ReceiveMessage): message is ReceiveNetworkMessage {
Expand Down
Loading
Loading