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.
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
vec2andvec4types 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 callcmul()andqmul().This presents immediate issues:
cmul()andqmul()instead of*and/to ensure proper multiplication for ℂ and ℍ, and use*and/for complex-scalar multiplications*or/operators are difficult to follow and very quickly lead to "parentheses hell"exp(),sin()etc. mathematical functions for ℂ and ℍThe programmer may consider aliasing
complex = vec2to help distinguish them in function signatures, but this offers no type safety. The programmer may consider creating a customstruct 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>andquat<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 WGSLvec2<T>andvec4<T>types.Creating a complex type is exactly like creating a vector type.
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.
For
complex<T>thexcomponent is the real component andyis the imaginary component. Forquat<T>,xyzare the ijk components andwis 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
zvia:It is an error to use a
complex<T>with aquat<T>. Likewise, it is an error to use acomplex<T>with avec2<T>or aquat<T>with avec4<T>.PI,TWOPI,TAU,EULER, etc., which even means...J. This proposal aims to be minimal and efficient. Zero-cost de-sugaring3f + 4f*Jwould 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 usecomplexf(3f, 4f).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 proposedcomplex<T>andquat<T>, because larger complex expressions become very un-ergonomic to type under a sea ofcmul()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 treatcomplex * complexandquat * quatas an error to stop the programmer from accidentally performing component-wise multiplication/division.Cast elision
Since
complex<T>andquat<T>lower to vector types, casts likecomplexf(foo(vec2f(z)))should lower tofoo(z)without the extravec2f(foo(vec2f(z)))noise especially whenzis already a vec2 after lowering.Promoted built-ins
A complex number and quaternion passed to component-wise
exp()orsin()orlog()without first casting tovec<T>is an error. However, we certainly want built-ins likelength(),normalize()anddot()to work natively on complex and quaternions without casting, and also built-ins likeround()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, likecmulT(),qmulT(),cdivT(),qdivT()for T=f,i,u variants. We may also want to detect simple squares likez*z->csquare(z)and1 / z->crecip(z)for efficiency.An explicit
conj()built-in to compute complex conjugates oncomplex<T>andquat<T>would be safer and less cryptic thanquatf(-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 theconj()function. This shouldn't be difficult to implement. Thoughts?An explicit
cmulj()andcmulnegj()to rotate acomplex<T>by +/- 90 degrees is also safer than swapping/negating components manually and can be done in-line as withconj(). This shouldn't be difficult to implement. Thoughts?Perhaps we could reserve these proposed built-ins in the meantime.