ultraviolet

repository·main·Indexed 21 days ago

https://github.com/fu5ha/ultraviolet

A high-performance linear and geometric algebra library for computer graphics and games. It features a unique 'wide' SIMD-based SoA (Structure of Arrays) architecture for runtime performance and uses Rotors from Geometric Algebra for rotations. The library provides standard scalar types and wide SIMD types (e.g., Vec3x8), along with support for integer vectors, f64 precision, and various interop features via Cargo flags.

Tokens
9K
Snippets
31
Records
38
Agent score
72%

What's inside ultraviolet

  1. Overview of ultraviolet

    main

    ultraviolet is a high-performance linear and geometric algebra library for computer graphics and games. It focuses on two main pillars:

    1. Productivity: The library avoids generics and complex Rust type-system hacks. This results in faster compilation times, straightforward interfaces, and clear, concise error messages.
    2. Runtime Performance: It provides two distinct types for most mathematical structures:
      • Standard types: Use usual scalar f32 values.
      • 'Wide' types: Use SIMD f32x4 vectors. These follow an SoA (Structure of Arrays) architecture, where a single wide type (e.g., Vec3x8) contains the data for multiple associated types (e.g., 8 Vec3s) and performs operations on all SIMD lanes simultaneously. This can be significantly faster (up to 10x) than standard AoS layouts for specific workloads.
  2. Handling branches in SIMD code using masks and blending

    main

    Standard if/else branching cannot be used directly in SIMD code because different lanes may need to follow different paths. Instead, use a mask-and-blend pattern:

    1. Generate Masks: Use comparison methods like .cmp_gt(value) to create a mask representing the condition for each lane.
    2. Combine Masks: Use bitwise operations like & (AND) to combine multiple conditions (e.g., mask_a & mask_b).
    3. Calculate All Paths: Calculate the results for both the true and false branches for all lanes.
    4. Blend Results: Use the .blend(true_value, false_value) method on the mask to select the correct result for each lane.

    This approach is highly effective when the cost of calculating both branches is outweighed by the performance gains of parallel processing.

    // 1. Create a mask for descrim > 0.0
    let desc_pos = descrim.cmp_gt(uv::f32x8::splat(0.0));
    
    // 2. Create a combined mask for t1 > 0.0 AND descrim > 0.0
    let t1_valid = t1.cmp_gt(uv::f32x8::splat(0.0)) & desc_pos;
    
    // 3. Use blend to select between the 'true' result and a 'false' fallback
    // This selects t1 if t1_valid is true, otherwise it selects the value 't'
    let t = t1_valid.blend(t1, t);
  3. How Rotors work in ultraviolet

    main

    Instead of using complex numbers (for 2D) or Quaternions (for 3D), ultraviolet uses Rotors from Geometric Algebra to represent rotations.

    Key Concepts

    • Rotor3: Use this type in place of a Quaternion. It is mathematically isomorphic to a Quaternion and performs the same functions, but is formulated through Geometric Algebra.
    • Rotation Model: Rotors treat rotation as something occurring within a plane rather than around an axis.
    • Generalization: Unlike Quaternions, Rotors can be generalized to 4 or higher dimensions (e.g., a Rotor4 could be implemented to handle 4D rotations).
  4. Porting scalar math to wide SIMD data types

    main

    When performing identical mathematical operations on multiple floating-point values without conditionals, you can achieve significant speed gains by porting from scalar types (like uv::Vec3) to wide data types (like uv::Vec3x8).

    To do this:

    1. Replace scalar types in function signatures with their wide counterparts (e.g., uv::Vec3 $\rightarrow$ uv::Vec3x8, f32 $\rightarrow$ f32x8).
    2. Use .splat(value) to populate wide types with a constant value across all lanes.
    3. Ensure your data structures (like Vec) are sized appropriately for the wide type (e.g., if using Vec3x8, your buffer size should ideally be a multiple of 8).

    Note: Handling 'remainder' elements (when the total count is not a multiple of the lane width) can be done via scalar fallback, narrower wide types (like Vec3x4), or by calculating extra vectors and ignoring the unused lanes.

    // Scalar setup
    let mut pos: Vec<uv::Vec3> = Vec::with_capacity(100);
    
    // Wide setup (8-lane)
    let mut pos: 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);
    pos.push(uv::Vec3x8::new(pos_x, pos_y, pos_z));
  5. Configure Cargo features for ultraviolet

    main

    By default, ultraviolet has a minimal feature set to improve build times. To enable specific functionality like f64 support, integer types, or interop with other crates, you must enable the corresponding feature flags in your Cargo.toml.

    Available Features

    FeatureDescription
    f64Enables f64 bit wide floating point support. Naming convention is D[Type], e.g., DVec3x4 is a collection of 4 3d vectors with f64 precision.
    intEnables integer vector types.
    bytemuckEnables casting many types to byte arrays, useful for graphics APIs.
    mintEnables interoperation with other math crates via the mint interface.
    num-traitsEnables identity traits for interoperation with other math crates.
    serdeEnables Serialize and Deserialize implementations for many scalar types.
  6. Understand the ultraviolet design philosophy

    main

    Core Principles

    ultraviolet is a linear and geometric algebra library designed for computer graphics and games, focusing on two main pillars:

    1. Productivity: The library avoids generics to ensure fast compilation times and clear, concise error messages. The interface is designed to be straightforward and easy to parse.
    2. Runtime Performance: It provides two distinct types for most operations:
      • Standard types: Use usual scalar f32 values.
      • 'Wide' types: Use SIMD vectors (e.g., f32x4, f32x8) to perform operations on multiple data points simultaneously.

    AoSoA (Array of Structs of Arrays) Architecture

    Wide types use an SoA (Structure of Arrays) architecture. A single wide data structure (like Vec3x8) contains the data for multiple associated types (in this case, 8 Vec3s) and performs operations on all SIMD 'lanes' at once. This can be significantly faster (up to a factor of 10) than standard AoS (Array of Structs) layouts, provided the algorithms are architected to handle the SIMD lanes without excessive branching.

  7. Example: SIMD Ray-Sphere Intersection

    main

    This example demonstrates how to port a scalar ray-sphere intersection algorithm to an 8-lane wide SIMD implementation using uv::Vec3x8 and uv::f32x8.

    fn ray_sphere_intersect_x8(
        sphere_o: uv::Vec3x8,
        sphere_r_sq: uv::f32x8,
        ray_o: uv::Vec3x8,
        ray_d: uv::Vec3x8,
    ) -> uv::f32x8 {
        let oc = ray_o - sphere_o;
        let b = oc.dot(ray_d);
        let c = oc.mag_sq() - sphere_r_sq;
        let descrim = b * b - c;
    
        let desc_pos = descrim.cmp_gt(uv::f32x8::splat(0.0));
        let desc_sqrt = descrim.sqrt();
    
        let t1 = -b - desc_sqrt;
        let t1_valid = t1.cmp_gt(uv::f32x8::splat(0.0)) & desc_pos;
    
        let t2 = -b + desc_sqrt;
        let t2_valid = t2.cmp_gt(uv::f32x8::splat(0.0)) & desc_pos;
    
        let t = t2_valid.blend(t2, uv::f32x8::splat(std::f32::MAX));
        let t = t1_valid.blend(t1, t);
    
        t
    }
  8. Convert floating-point vectors to integer vectors using TryFrom

    main

    To convert a floating-point vector to an integer vector safely, use the TryFrom trait. This performs a lossy conversion by flooring the float values. The conversion is supported for the following mappings:

    Standard (f32):

    • Vec2 $\rightarrow$ IVec2 or UVec2
    • Vec3 $\rightarrow$ IVec3 or UVec3
    • Vec4 $\rightarrow$ IVec4 or UVec4

    Double Precision (f64) - requires f64 feature:

    • DVec2 $\rightarrow$ IVec2 or UVec2
    • DVec3 $\rightarrow$ IVec3 or UVec3
    • DVec4 $\rightarrow$ IVec4 or UVec4

    Note: Converting to UVec types with negative float components will result in a FloatConversionError::NegOverflow error.

    use core::convert::TryFrom;
    
    // Example: Converting Vec2 to IVec2
    let vec2 = Vec2::new(1.99, 2.99);
    let ivec2 = IVec2::try_from(vec2); 
    assert_eq!(ivec2.unwrap(), IVec2::new(1, 2));
    
    // Example: Handling overflow/invalid values
    let vec2_nan = Vec2::new(f32::NAN, 0.0);
    let result = IVec2::try_from(vec2_nan);
    assert_eq!(result.err(), Some(FloatConversionError::NaN));
  9. Generate Perspective Projection Matrices with Reversed and Infinite Z-Axis

    main

    This combines Reversed-Z and Infinite-Z for the best possible precision in extremely large scenes. It provides high precision (via Reversed-Z) and eliminates far-plane numerical issues (via Infinite-Z).

    API Variants:

    • perspective_reversed_infinite_z_wgpu_dx_gl: For WebGPU, DirectX, or OpenGL. Destination is left-handed, y-up. Z clip: [0.0, 1.0].
      • Note for OpenGL: Requires the gl_arb_clip_control extension to set the z clip from [-1.0, 1.0] to [0.0, 1.0].
    • perspective_reversed_infinite_z_vk: For Vulkan. Destination is right-handed, y-down. Z clip: [0.0, 1.0].

    Note: These functions only require vertical_fov, aspect_ratio, and z_near.

    // Example for WebGPU/DirectX
    let mat = perspective_reversed_infinite_z_wgpu_dx_gl(fov_rad, aspect, near);
  10. Use 2D integer vectors (ivec2)

    main

    The ivec2 type represents a set of two coordinates (x, y) in 2D space. It can be used as a vector or a point. It supports standard arithmetic operations, dot products, and conversions to/from homogeneous coordinates.

    Homogeneous Conversions

    • Points: Use into_homogeneous_point() to create a 3D homogeneous point (where the z component is 1).
    • Vectors: Use into_homogeneous_vector() to create a 3D homogeneous vector (where the z component is 0).
    • From Homogeneous: Use from_homogeneous_point(v: $v3t) to convert back to 2D by dividing by the homogeneous component (use only for points). Use from_homogeneous_vector(v: $v3t) to discard the homogeneous component.
    let v = ivec2::new(1, 2);
    let point_h = v.into_homogeneous_point(); // { x: 1, y: 2, z: 1 }
    let vec_h = v.into_homogeneous_vector();   // { x: 1, y: 2, z: 0 }
    
    let dot = v.dot(ivec2::unit_x());
    let mag = v.mag();
    let mag_sq = v.mag_sq();
  11. Convert integer vectors to floating-point vectors using From

    main

    You can convert integer vectors back to floating-point vectors using the From trait. This is a lossless conversion that casts the integer components to the corresponding float type.

    Mappings:

    • IVec2, IVec3, IVec4 $\rightarrow$ Vec2, Vec3, Vec4 (f32)
    • UVec2, UVec3, UVec4 $\rightarrow$ Vec2, Vec3, Vec4 (f32)
    • IVec2, IVec3, IVec4 $\rightarrow$ DVec2, DVec3, DVec4 (f64) — requires f64 feature
    • UVec2, UVec3, UVec4 $\rightarrow$ DVec2, DVec3, DVec4 (f64) — requires f64 feature
    let ivec2 = IVec2::new(1, 2);
    let vec2 = Vec2::from(ivec2);
    assert_eq!(vec2, Vec2::new(1.0, 2.0));
  12. Generate Left-Handed Y-Up Orthographic Projection Matrices

    main

    Use these functions to create orthographic projection matrices when your application's coordinate system assumes +X is right, +Y is up, and +Z points away from the viewer. The choice of function depends on your target graphics API and its expected clip space:

    • orthographic_gl: For OpenGL. Destination is left-handed, y-up. Z clip: [-1.0, 1.0].
    • orthographic_vk: For Vulkan. Destination is right-handed, y-down. Z clip: [0.0, 1.0].
    • orthographic_wgpu_dx: For WebGPU or DirectX. Destination is left-handed, y-up. Z clip: [0.0, 1.0].
    // Example for OpenGL
    let mat = orthographic_gl(left, right, bottom, top, near, far);
    
    // Example for Vulkan
    let mat = orthographic_vk(left, right, bottom, top, near, far);
    
    // Example for WebGPU/DirectX
    let mat = orthographic_wgpu_dx(left, right, bottom, top, near, far);