Crate lightwalk

Crate lightwalk 

Source
Expand description

WARNING: This project is super early in development does not do everything yet, and contain bugs. It will evolve and break frequently. Don’t use it in a serious project (yet)! Feedback and issues are obviously very welcome.

Examples are provided in the repo

The General Purpose Signed Distance Functions (GPSDF) crate aims to provide a simple yet powerful API for handling signed distance functions (see the wikipedia article for more information) directly in rust projects. This crate provides a few key features:

  • The ability to expressively define SDFs by leveraging rust’s type system, in any dimension, with any scalar type (realistically either f32 or f64) and with arbitrarily bound data.
  • The ability to combine and transform SDFs
  • The ability to retrieve distance information and state information for any SDF.

NOTE: This crate relies heavily on rust’s compile-time optimizations, especially the monomorphization of (aggressively) inlined functions, and it abuses the type system by pushing it to it’s limits to generate efficient distance and state functions and minimal memory footprint. This may have an impact on compile times, even though as of yet I haven’t benchmarked this.

§Architecture

The crate recognizes three distinct categories of SDFs:

  1. Primitives: stripped down SDFs defining a shape in as little information as reasonably possible (typically 0 bytes), typically centered at the origin, and typically with normalized dimensions (insofar as that applies,) and are stateless. Examples of basic primitives include:

    • Nothing (a infinite distance everywhere)
    • A sphere of radius 1 centered at the origin
    • A axis-aligned cube of side length 1 centered at the origin

    The library also defines some primitive generators, like the cuboid function, that takes in as arguments its side-lengths along each axis, and returns the cuboid primitive. These generators exist both due to convenience and due to the lack of a non-homogeneous scaling transformer (we can’t independently scale each axis of a SDF, and thus a simple cube primitive does not suffice.) The crate does not make the distinction between true primitives and the result of a primitive generator.

  2. Transformers: A wrapper around a SDF that modifies the distance and state calculations in some meaningful way, frequently inflating the memory footprint of the SDF in the process. Examples include:

    • Translation
    • Scaling
    • Inverting
    • State binding (only for stateless SDFs)
  3. Combinators: The culmination of multiple SDFs into one. These can either be:

    • Heterogeneous binary combinators: combining two SDFs of different types, for example a sphere and a cube (note that the scalar field, state and dimension must remain consistent between both sub SDFs.) Examples include:

      • Binary Union (hard & soft)
      • Binary Intersection (hard & soft)
      • Binary Difference (hard & soft)
    • Homogeneous n-ary combinators: combining arbitrary many SDFS of identical types, but who’s parameters can vary; for example multiple spheres translated by differing vectors. Examples include:

      • Container Combiners, who hold a collection of SDFs stored in memory (for example in a vector or an array). This variant takes up more space, but is able to cache some information (and thus tends to be faster).
      • Iterator Combiners, who simply hold an iterator over SDFs. This variant is less memory intensive, as no child SDF is actually stored in memory, however no caching can be performed as a result, leading to poorer performance at evaluation.

      Regardless of it’s type, n-ary combiners provide implementations for soft & hard union and intersections (but not differences, as that operation being neither commutative or associative makes it’s implementation somewhat unclear.) They also require sub SDFs to be of identical scalar type, dimension and state (which is enforced by their homogeneity.)

§Usage

All functions, types and traits that should be used by an end user are exposed in the prelude module, and globally importing the module is the intended way to use the library.

Primitives can either be:

  • Unit structs, in which case they should just be used as is
  • Tuple structs or record structs, in which case an associated function is provided (for example, to create a cuboid, we should call the cuboid function.) A complete list of primitives and their associated functions is present here)

Transformers and combiners should not be constructed directly, and instead the user is recommended to use one of the operation traits provided. These include:

§Examples

Here are a few gibberish examples to illustrate how to use the library:

use lightwalk::prelude::*;

// This shows a 2D SDF

// plane(0) is a plane pointing in the x direction.
let sdf = Sphere.add(plane(0)).translate([1.0, 2.0]).mul(Sphere.scale(3.0).invert());
let distance = sdf.distance([-2.0, 3.0]);

let gradient = sdf.gradient([-3.0, 2.0], 1e-3);

// direction of the gradient of the SDF (should be the same as the gradient, as well-formed
// SDF's gradients should be normalized, but in case this isn't the case, we here normalize it)
let normal = sdf.normal([-3.0, 2.0], 1e-3);

Here we define a state we wish our SDFs to have:

use lightwalk::prelude::*;

// This shows a 3D SDF

// We give each SDF a value (in practice, this could be a color, or a material)
#[derive(Clone, Default)]
struct State(f32);

impl SdfState<f32, 3> for State {
    type Sample = f32;

    #[inline]
    fn mix(&self, other: &Self, factor: f32) -> Self {
        // Basic linear interpolation
        Self(self.0 + (other.0 - self.0) * factor)
    }

    #[inline]
    fn sample(&self, _: &[f32; 3]) -> f32 {
        self.0
    }
}

// We can bind a state to a SDF
let cube = Cube.bind(State(2.0));
let sphere = Sphere.bind(State(3.0));

// To join multiple SDFs, they all need to have the same state (in addition
// to having the same scalar type and dimension, of course)
let sdf = cube.add(sphere);

// Depending on where we sample, we will either get a state of 2 or of 3
// (because this is a hard union, there is no state blending, so no values
// between 2.0 and 3.0)
let state = sdf.state([1.0, 2.0, 3.0]);

Modules§

combinators
macros
math
prelude
primitives
transformers

Traits§

Sdf
Base trait for objects that represent a signed distance function.
SdfState
Trait corresponding to types that can be bound to an SDF.