Skip to content

Complex numbers and quaternions #192

Description

@jordanhalase

Motivation

Complex numbers (ℂ) and quaternions (ℍ) often show up in computer graphics for rotations, SLERP interpolation, skeletal animation, FFT ocean simulation, fractal visualization (e.g., Mandelbrot set), procedural textures via noise synthesis, abstract psychedelic visual effects, or GPU-accelerated complex analysis.

Historically, shading languages have not supported explicit complex number types, so computer graphics programmers use the native vec2 and vec4 types in lieu of them. This generally works because complex numbers and quaternions share the same rules as vectors for addition and scalar multiplication.

However, multiplication in ℂ and ℍ is different than for vectors. In shading languages, operators like * and / for vector-vector are defined component-wise, which is wrong for complex-complex or quaternion-quaternion multiplication. As a result, programmers typically implement their own functions, which we will call cmul() and qmul().

This presents immediate issues:

  • The programmer must remember to call cmul() and qmul() instead of * and / to ensure proper multiplication for ℂ and ℍ, and use * and / for complex-scalar multiplications
  • Mathematical expressions using function calls instead of infix * or / operators are difficult to follow and very quickly lead to "parentheses hell"
  • The programmer must remember not to call component-wise exp(), sin() etc. mathematical functions for ℂ and ℍ
  • The programmer must remember which active vector-type variables are actually a vector rather than a complex number

The programmer may consider aliasing complex = vec2 to help distinguish them in function signatures, but this offers no type safety. The programmer may consider creating a custom struct complex {...} type, but this is inefficient and loses all */+- operators, making expressions even more difficult to follow.

Proposal

I would like to introduce explicit complex<T> and quat<T> types to the WESL shading language, including their (complex|quat)(f|i|u) aliases, type safety with explicit casts between their vector counterparts, and lowers into plain WGSL vec2<T> and vec4<T> types.

Creating a complex type is exactly like creating a vector type.

let z = complex<f32>(3f, 4f); // Can also use complexf()
let q = quat<f32>(3f, 2f, 1f, 4f); // Can also use quatf()

Complex types can be swizzled just like their corresponding vector types. A swizzle on a complex type produces a vector type, or scalar type for a single swizzle.

let z = complexf(3f, 4f);
let v: vec2<f32> = z.xy; // z.xy is vec2<f32>

For complex<T> the x component is the real component and y is the imaginary component. For quat<T>, xyz are the ijk components and w is the real component.

Mathematically, the quaternions are a superset of the complex numbers, however, their layout in computer memory is usually different. A quaternion can be created from a complex number z via:

let q = quatf(z.y, 0, 0, z.x);

It is an error to use a complex<T> with a quat<T>. Likewise, it is an error to use a complex<T> with a vec2<T> or a quat<T> with a vec4<T>.

⚠️ Explicit non-goals

  • No constants, so no PI, TWOPI, TAU, EULER, etc., which even means...
  • ...no complex unit J. This proposal aims to be minimal and efficient. Zero-cost de-sugaring 3f + 4f*J would require multiple steps and intermediate abstract types for debatably minimal ergonomic gain, and the use of + and * only to be elided can be confusing. Please use complexf(3f, 4f).
  • No algorithms, so no SLERP, no FFT, no quaternions from euler angles, no rotation matrices from quaternions, etc.

This extension would provide basic complex number support to WESL and WGSL to become the foundation for the algorithms above to be easier implemented and with more type safety.

TBD

Infix multiplication and division

My primary motivation for this outside of vec/complex type safety is to introduce infix * and / to the proposed complex<T> and quat<T>, because larger complex expressions become very un-ergonomic to type under a sea of cmul() calls.

I realize this may be controversial, but I hope to at least get you on-board with overloading * and / just for the proposed complex/quat types. In the meantime, we can treat complex * complex and quat * quat as an error to stop the programmer from accidentally performing component-wise multiplication/division.

Cast elision

Since complex<T> and quat<T> lower to vector types, casts like complexf(foo(vec2f(z))) should lower to foo(z) without the extra vec2f(foo(vec2f(z))) noise especially when z is already a vec2 after lowering.

Promoted built-ins

A complex number and quaternion passed to component-wise exp() or sin() or log() without first casting to vec<T> is an error. However, we certainly want built-ins like length(), normalize() and dot() to work natively on complex and quaternions without casting, and also built-ins like round() and honestly most component-wise built-ins should be promoted and only exclude the ones where multiplication act differently (no exp, no log, no sin, etc).

New built-ins

If we do use infix * and /, we will need functions they will lower to, like cmulT(), qmulT(), cdivT(), qdivT() for T=f,i,u variants. We may also want to detect simple squares like z*z -> csquare(z) and 1 / z -> crecip(z) for efficiency.

An explicit conj() built-in to compute complex conjugates on complex<T> and quat<T> would be safer and less cryptic than quatf(-q.xyz, q.w). If the argument expression is a single variable (rather than a longer expression), or a single variable under a unary -, it could be lowered in-line without actually calling the conj() function. This shouldn't be difficult to implement. Thoughts?

An explicit cmulj() and cmulnegj() to rotate a complex<T> by +/- 90 degrees is also safer than swapping/negating components manually and can be done in-line as with conj(). This shouldn't be difficult to implement. Thoughts?

Perhaps we could reserve these proposed built-ins in the meantime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions