Ultraviolet
A wide linear algebra crate for games and graphics.
Install / Use
/learn @fu5ha/UltravioletREADME
ultraviolet
This is a crate to computer-graphics and games-related linear and geometric algebra, but fast, both in terms of productivity and in terms of runtime performance.
In terms of productivity, ultraviolet uses no generics and is designed to be as straightforward of an interface as possible, resulting in fast compilation times and clear code. In addition, the lack of generics and Rust type-system "hacks" result in clear and concise errors that are easy to parse and fix for the user.
In terms of runtime performance, ultraviolet was designed from the start with performance in mind. To do so, we provide two separate kinds of each type, each with nearly identical functionality, one with usual scalar f32 values, and the other a 'wide' type which uses SIMD f32x4 vectors for each value. This design is clear and explicit in intent, and it also allows code to take full advantage of SIMD.
The 'wide' types use an "SoA" (Structure of Arrays) architecture
such that each wide data structure actually contains the data for 4 or 8 of its associated data type and will do any operation
on all of the simd 'lanes' at the same time. For example, a Vec3x8 is equivalent to 8 Vec3s all bundled together into one
data structure.
Doing this is potentially much (factor of 10) faster than an standard "AoS" (Array of Structs) layout, though it does depend on your workload and algorithm requirements. Algorithms must be carefully architected to take full advantage of this, and doing so can be easier said than done, especially if your algorithm involves significant branching.
ultraviolet was the first Rust math library to be designed in this "AoSoA" manner, though
nalgebra now supports it for several of their data structures as well.
Benchmarks
See mathbench-rs for latest benchmarks.
Cargo Features
To help further improve build times, ultraviolet puts various functionality under feature flags. For example, the 2d and 3d projective geometric algebras
as well as f64 and integer types are disabled by default. In order to enable them, enable the corresponding crate feature flags in your Cargo.toml.
Here's a list of the available features:
f64– Enablef64bit wide floating point support. Naming convention isD[Type], such asDVec3x4would be a collection of 4 3d vectors withf64precision each.int– Enable integer vector types.bytemuck– Enable casting of many types to byte arrays, for use with graphics APIs.mint– Enable interoperation with other math crates through themintinterface.num-traits– Enable identity traits for interoperation with other math crates.serde– EnableSerializeandDeserializeimplementations for many scalar types.
Crate Features
This crate is currently being dogfooded in my ray tracer rayn,
and is being used by various independent Rust game developers for various projects.
It does what those users have currently needed it to do.
There are a couple relatively unique/novel features in this library, the most important being the use of the Geometric Algebra.
Instead of implementing complex number algebra (for 2d rotations) and Quaternion algebra (for 3d rotations), we use Rotors, a concept taken from Geometric Algebra, to represent 2d and 3d rotations.
What this means for the programmer is that you will be using the Rotor3 type in place of
a Quaternion, though you can expect it to do basically all the same things that a Quaternion does. In fact, Quaternions
are directly isomorphic to Rotors (meaning they are in essense the same thing, just formulated differently). The reason this decision was made was twofold:
first, the derivation of the math is actually quite simple to understand. All the derivations for the code implemented in the Rotor structs in this
library are written out in the derivations folder of the GitHub repo; I derived them manually as part of the implementation.
On the other hand, Quaternions are often basically just seen as black boxes that we programmers use to do rotations because they have some nice properties, but that we don't really understand. You can use Rotors this same way, but you can also easily understand them. Second is that in some sense they can be seen as 'more correct' than Quaternions. Specifically, they facilitate a more proper understanding of rotation as being something that occurs within a plane rather than something that occurs around an axis, as it is generally thought. Finally, Rotors also generalize to 4 and even higher dimensions, and if someone wants to they could implement a Rotor4 which retains all the properties of a Rotor3/Quaternion but does rotation in 4 dimensions instead, something which simply is not possible to do with Quaternions.
If it's missing something you need it to do, bug me on the GitHub issue tracker and/or Rust community discord server (I'm Fusha there) and I'll try to add it for you, if I believe it fits with the vision of the lib :)
Examples
Euler Integration
Euler Integration is a method for numerically solving ordinary differential equations. If that sounds complicated, don't worry! The details of the method don't matter if you're not looking to implement any kind of physics simulation but this method is common in games. Keep reading for the code below!
The point is that if you are doing the same basic math operations on multiple floating point values with no conditionals (no ifs), porting to wide data types and parallel processing is quite simple.
Here is the scalar example of Euler Integration:
fn integrate(
pos: &mut [uv::Vec3],
vel: &mut [uv::Vec3],
acc: &[uv::Vec3],
dt: f32,
) {
for ((position, velocity), acceleration) in pos.iter_mut().zip(vel).zip(acc) {
*velocity = *velocity + *acceleration * dt;
*position = *position + *velocity * dt;
}
}
The code loops over each set of corresponding position, velocity, and acceleration vectors. It first adjusts the velocity by the acceleration scaled by the amount of time that has passed and then adjusts the position by the velocity scaled by the amount of time that has passed.
These are all multiplication, addition, and assignment operators that need to be applied in the same way to all of the variables in question.
To port this function to wide data types and parallel processing, all we have to do is change the data types and we're done! The new function looks like this:
fn integrate_x8(
pos: &mut [uv::Vec3x8],
vel: &mut [uv::Vec3x8],
acc: &[uv::Vec3x8],
dt: f32x8,
) {
for ((position, velocity), acceleration) in pos.iter_mut().zip(vel).zip(acc) {
*velocity = *velocity + *acceleration * dt;
*position = *position + *velocity * dt;
}
}
This function now processes 8 sets of vectors in parallel and brings significant speed gains!
The only caveat is that the calling code that creates the slices of vectors needs to be modified to populate these wide data types with 8 sets of values instead of just one. The scalar code for that looks like this:
let mut pos: Vec<uv::Vec3> = Vec::with_capacity(100);
let mut vel: Vec<uv::Vec3> = Vec::with_capacity(100);
let mut acc: Vec<uv::Vec3> = Vec::with_capacity(100);
// You would probably write these constant values in-line but
// they are here for illustrative purposes
let pos_x = 1.0f32;
let pos_y = 2.0f32;
let pos_z = 3.0f32;
let vel_x = 4.0f32;
let vel_y = 5.0f32;
let vel_z = 6.0f32;
let acc_x = 7.0f32;
let acc_y = 8.0f32;
let acc_z = 9.0f32;
for ((position, velocity), acceleration) in pos.iter_mut().zip(vel).zip(acc) {
pos.push(uv::Vec3::new(pos_x, pos_y, pos_z));
vel.push(uv::Vec3::new(vel_x, vel_y, vel_z));
acc.push(uv::Vec3::new(acc_x, acc_y, acc_z));
}
Whereas to populate the same for the 8-lane wide Vec3x8 data type, the code could look like this:
let mut pos: Vec<uv::Vec3x8> = Vec::with_capacity(100 / 8 + 1);
let mut vel: Vec<uv::Vec3x8> = Vec::with_capacity(100 / 8 + 1);
let mut acc: Vec<uv::Vec3x8> = Vec::with_capacity(100 / 8 + 1);
let pos_x = uv::f32x8::splat(1.0f32);
let pos_y = uv::f32x8::splat(2.0f32);
let pos_z = uv::f32x8::splat(3.0f32);
let vel_x = uv::f32x8::splat(4.0f32);
let vel_y = uv::f32x8::splat(5.0f32);
let vel_z = uv::f32x8::splat(6.0f32);
let acc_x = uv::f32x8::splat(7.0f32);
let acc_y = uv::f32x8::splat(8.0f32);
let acc_z = uv::f32x8::splat(9.0f32);
for ((position, velocity), acceleration) in pos.iter_mut().zip(vel).zip(acc) {
pos.push(uv::Vec3x8::new(pos_x, pos_y, pos_z));
vel.push(uv::Vec3x8::new(vel_x, vel_y, vel_z));
acc.push(uv::Vec3x8::new(acc_x, acc_y, acc_z));
}
Note that 100 / 8 in maths terms would be 12.5, but we can't conveniently have a half-sized Vec3x8.
There are various ways to handle these 'remainder' vectors. You could fall back to scalar code, or progressively fall back to narrower wide types, such as Vec3x4, or you can just consider whether the cost of calculating a few additional vectors that you won't use is worth adding complexity to your code.
Ray-Sphere Intersection
Scalar code that operates on a single value at a time needs some restructuring to take advantage of SIMD and the 4-/8-wide data types.
Below is an example of scalar ray-sphere instersection code using Vec3 for points and vectors:
fn ray_sphere_intersect(
ray_o: uv::Vec3,
ray_d: uv::Vec3,
sphere_o: uv::Vec3,
sphere_r_sq: f32,
) -> f32 {
let oc = ray_o - sphere_o;
let b = oc.dot(ray_d);
let c = oc.mag
Related Skills
node-connect
339.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
339.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.9kCommit, push, and open a PR
