Standardization beyond colors and vectors (i.e. Matrix, Scalar, Quaternion)

TL;DR:

Tensor would be added, which provides a standardized API for interacting with vectors, colors, matrices, quaternions, etc.

Rational

After merging #13699, the various vectors implement Vector. Vector, along with the color classes implement VectorLike. VectorLike defines a standard signature for different methods (e.g. copyFrom, clone, add, subtractInPlaceFromFloats, etc.). Vector extends VectorLike and adds normalization methods and length / lengthSquared. This proposal’s goals are based on the ideas behind #13699, with the core idea being to create a standard interface from which various math constructs are defined.

Semantics

Note:

  • CurrentClass is a placeholder for a class that implements Tensor. In fields, it is the current class.
  • Tensor and its related types will be defined in src/Maths/tensor.ts unless otherwise noted

MultidimensionalArray

Defined in src/types.ts

type MultidimensionalArray<T, D extends number> = D extends 1
	? T[]
	: MultidimensionalArray<T, D - 1>[];

Represents a multidimensional array of T with depth D.

TensorValue

type TensorValue = number | MultidimensionalArray<TensorValue, number>

Tensor

declare class Tensor<V extends TensorValue>

This proposal replaces VectorLike with a new type, Tensor, for tensor-like classes to implement. Vector would be changed to extend Tensor,

Tensor includes all of the methods in VectorLike. This includes

  • Math operations (add, subtract, multiply, divide, scale, …)
  • Array conversion (fromArray, toArray, asArray, …)
    • Since the type parameter is no longer assignable to number[], the return type for array-related methods will be changed to number[]
    • See Tensor.value
  • Transfering (clone, copyFrom, copyFromFloats, …)
  • The above methods’ InPlace, ToRef, FromFloats, etc.

It adds or changes the following methods:

Tensor.From

public static From(source: Tensor, fillValue: number = 0): CurrentClass;

From creates a new instance of CurrentClass from source.
source: The tensor to copy data from.
fillValue: The value to use for filling empty parts of the resulting CurrentClass.

Example:

const vec3 = new Vector3(1, 2, 3);
const vec4 = Vector4.From(vec3, 4); // { 1, 2, 3, 4 }
const matrix = Matrix.From(vec3, 0); // this is possible!

Additionally, From could be changed to be From(...args: [...Tensor[], number]): CurrentClass. This is better understood by this invalid Typescript signature (since rest parameters must be last):

public static From(...source: Tensor[], fillValue: number = 0): CurrentClass;

Tensor.as

public as<T extends typeof Tensor>(type: T, fillValue: number = 0): InstanceType<T>;

as creates an instance of type from an instance of CurrentClass.
type: The class to create an instance of.
fillValue: The value to use for filling empty parts of the resulting instance.

Example:

const vec3 = new Vector3(1, 2, 3);
const vec4 = vec3.as(Vector4, 4); // { 1, 2, 3, 4 }
const matrix = vec3.as(Matrix, 0);

Tensor.sum

public sum(): number;

sum return the sum of the components of the Tensor.

Example:

const scalar = new Scalar(1), 
	vec3 = new Vector3(1, 2, 3),
	vec4 = new Vector4(4, 5, 6, 7),
	matrix = new Matrix();

scalar.sum() // 1
scalar.sum() // 6
scalar.sum() // 22
scalar.sum() // 0

:warning: The return value of the lengthSquared or length method of vectors is not the same as the return value of sum.

Tensor.rank

public abstract rank: number;

The rank of a tensor is the number of indices required to uniquely select each element of the tensor.

🛈 The rank of a Tensor is the same as the length of its dimension.

Example:

const scalar = new Scalar(), 
	vec3 = new Vector3(),
	matrix = new Matrix();

scalar.rank // 0, since indicies are not needed
vec3.rank // 1
matrix.rank // 2

Tensor.dimension

public abstract dimension: number[];

dimension is the mathematical dimension [2] of the tensor. In dynamic tensor types, it can be defined as a getter.

Example:

const scalar = new Scalar(),
	vec3 = new Vector3(),
	vec4 = new Vector4(),
	matrix = new Matrix();

scalar.dimension // []
vec3.dimension // [3]
vec4.dimension // [4]
matrix.dimension // [4, 4]

Tensor.value

public get value(): V;
public set value(value: V): void;

value is the values of the tensor in a multidimensional array with the type V (the type parameter of the class). Unlike dimension, this must be implemented as a getter and setter.

Example:

const vec3 = new Vector3(),
	matrix = new Matrix();

vec3.value
// [0, 0, 0]

matrix.value
// [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ] ]

vec3.value = [ 1, 2, 3 ];
vec3.x // 1

isTensor

function isTensor(value: unknown): value is Tensor;

Checks if a value is a Tensor.
Example:

const vec3 = new Vector3();
isTensor(vec3) // true
isTensor(vec3.value) // false
isTensor([1, 2, 3]) // false
isTensor(0) // false
isTensor(null) // false

isTensorValue

function isTensorValue(value: unknown): value is TensorValue;

Checks if a value is a TensorValue.
Example:

const vec3 = new Vector3();
isTensorValue(vec3) // false
isTensorValue(vec3.value) // true
isTensorValue([1, 2, 3]) // true
isTensorValue(0) // true
isTensorValue(null) // false

getDimension

function getDimension(value: Tensor | TensorValue): number[];

getDimension returns the mathimatical dimension [2] of value, similar to Tensor.dimension. If value is not a Tensor or TensorValue, it will throw a TypeError

🛈 Tensor based classes can use getDimension(this.value) for their dimension implementations.

Considerations

  1. Static methods not pertaining to data transfer and formatting (e.g. Add, Normalize, Lerp) and non-static normalization methods are outside the scope of this proposal. While Tensor may include them in the future, this proposal does not
  2. This proposal does not provide for a dimensionally dynamic Tensor.

Performance

Since Tensor is defined using the declare class and classes that follow Tensor do so using the implements keyword, there are no runtime changes to Tensor-based classes.

Bundle size

The JS bundle size will increase only by the size of the added functions (isTensor, getDimension, etc.). The TS declaration size will also increase due to Tensor.

Questions

  1. Should Scalar be included in this proposal? As a rank 0 tensor, the benefits of standardizing Scalar may not be worth it, especially considering it is not currently meant to be instantiated.

  2. How should incompatibility be managed? Should incompatible classes be dropped or should they be modified to follow Tensor?

  3. What other classes (e.g. Size) would be included/standardized?

FAQs

None yet

Revisions

If revisions to this proposal are made, edits will be made to the above proposal with revision numbers attached to revised semantics, questions, considerations, and FAQs. Revisions were made on the below dates:

#0: 23 August 2023

Further reading

  1. Tensor - Wikipedia
  2. Dimension - Wikipedia

I am not opposed but really want to see a concrete use case and usage first to justify the extra maintenance and size ?

Take the current difference in behavior between vectors and colors:

const c1 = Color3.Random(),
	c2 = Color3.Random(),
	c3 = new Color3(),
	v1 = Vector3.Random(),
	v2 = Vector3.Random(),
	v3 = new Vector3();

c1.addToRef(c2, c3) === c3 // false
v1.addToRef(v2, v3) === v3 // true

And the lack of a method that should exist in Color3:

const v1 = Vector3.Random(),
	v2 = Vector3.Random(),
	c1 = Color3.Random(),
	c2 = Color3.Random();


v1.addInPlace(v2) // ok
c1.addInPlace(c2) // ReferenceError! addInPlace does not exist

Tensor provides a single interface for tensor-like objects which users can depend on. By having tensor-like classes implement Tensor, it ensures that all the methods exist and have the same and correct behavior. It is meant for organization and standardization. Since there is no implementation (just a class declaration), the extra maintenance is minimized to member signatures. The benefits of using Tensor far outweigh the potential for differentiating behavior and interfaces, to include the complete lack of methods that should exist.

I am not sure to understand the difference with your current PR aside of the naming in this case ?

The primary difference is with how dimensions are handled. With Tensor, it is generalized further. A mathematical vector can be represented as number[]. A mathematical tensor is represented as a TensorValue (from the proposal). I’ve created this table to make sense of it:

tensor rank TS representation BJS class (if exists)
0 number Scalar
1 number[] Vector, DynamicVector
1 [number, number] Vector2
1 [number, number, number] Vector3, Color3
1 [number, number, number, number] Vector4, Color4, Quaternion
2 number[][] Matrix
3 number[][][]
4 number[][][][]
N TensorValue

Also, @sebavan, what do you think about me closing #13699 and moving Vector into the new PR along with the Tensor types? I think it would be good to clear the 100+ comments in #13699 and start from a fresh PR. I would keep the commits I’ve already made.

Yup but in this case as you do it mostly for consistency in the API, why having both Vector and Tensor as declared types ?

Vector added normalization methods (i.e. normalize, normalizeFromLength, normalizeToRef, etc.), length, and lengthSquared. This change was made because you brought this up in the PR:

I do not know if color and vector needs to be that close. (they could but I am wondering if they need to, for instance, what does length represent for a color ?

Which makes sense. Furthering your point, what would length represent for a Tensor? There is simply no use or meaning for length when abstracted to that level.

Ok so basically vector and color would inherit/implement tensor instead of color implementing vectorLike ???

Yes, you could think of it like this

TensorScalar
TensorVectorVectorN
TensorColorN
TensorQuaternion
TensorMatrix

When this plan will be realized, this design is useful

1 Like