mkeeter / fidget

blazing fast implicit surface evaluation
Mozilla Public License 2.0
123 stars 9 forks source link

Shapes library? #145

Open waywardmonkeys opened 2 months ago

waywardmonkeys commented 2 months ago

Would there be interest in having a collection of the more common SDF shapes that create trees to give a slightly higher level API for using this?

LukeP0WERS commented 2 months ago

I created a somewhat shitty crate which makes shape construction easier with vector types: https://github.com/LukeP0WERS/fidget_math My SDF modeler only has boxes and spheres at the moment, but I will eventually be adding most if not all of the common SDF shapes that can be represented with fidget. Since the TreeVec3 type was made specifically for shape construction I think it would make sense to add some shapes to that crate if you want me to. For example this is what a cube looks like normally:

fn cube(pos: [Tree; 3], size: [Tree; 3]) -> Tree {
    let q = [
        pos[0].abs() - size[0].clone(),
        pos[1].abs() - size[1].clone(),
        pos[2].abs() - size[2].clone(),
    ];
    let q_length = (
        q[0].max([0.0](f64::EPSILON)).square() +
        q[1].max([0.0](f64::EPSILON)).square() +
        q[2].max([0.0](f64::EPSILON)).square()
    ).sqrt();

    return q_length + q[0].clone().max(q[1].clone().max(q[2].clone())).min(0.0);
}

and this is what it looks like with my crate:

pub fn cube (pos: TreeVec3, size: TreeVec3) -> Tree {
    let q = pos.abs() - size;
    return q.clone().max(TreeVec3::splat(f64::EPSILON)).length() + q.max_element().min(0.0);
}

If this sounds useful to you let me know and I can add this in.

mkeeter commented 2 months ago

This has been on my mind, but I have more questions than answers at the moment.

I started making a shapes library in core.rhai, but suspect that's not the right place for it; we probably want a Rust module that exports Rhai bindings.

I agree with @LukeP0WERS that we probably want TreeVec2/3 types for arguments, and they should support all of the operator overloading that you'd expect. Interestingly, this may Just Work using nalgebra:

    #[test]
    fn nalgebra_tree() {
        let x = Tree::x();
        let y = Tree::y();
        let z = Tree::z();
        let v = nalgebra::Vector3::new(x, y, z);
        let q = nalgebra::Vector3::new(
            Tree::constant(1.0),
            Tree::constant(2.0),
            Tree::constant(3.0),
        );
        let out = v + q;
        println!("{out:?}");
    }

The lack of default and named arguments in Rust functions makes things a bit awkward, e.g. lots of functions just take vec2, vec2.

A different option would be to declare a trait and build an explicit object tree:

trait ShapeLike {
    fn get(&self, ctx: &mut Context, axes: [Node; 3]) -> Node;
}

struct Circle {
    center: TVec3,
    radius: TFloat,
}

impl ShapeLike for Circle {
    fn get(&self, ctx: &mut Context, axes: [Node; 3]) -> Node {
        // ... etc
}

struct Union {
    lhs: Box<dyn ShapeLike>,
    rhs: Box<dyn ShapeLike>,
}

impl ShapeLike for Union {
    fn get(&self, ctx: &mut Context, axes: [Node; 3]) -> Node {
        let lhs = self.lhs.get(ctx, axes);
        let rhs = self.rhs.get(ctx, axes);
        ctx.min(lhs, rhs).unwrap()
    }
}

(yes, yes, this would be a third way to build shapes, along with Tree and Context)

As another data point, libfive used a terrible homebrew syntax to automatically generate Scheme and Python bindings from a C header, documented in this README.

LukeP0WERS commented 2 months ago

Right now I have a fully functional modeler with domain operations, shapes, and combination operations working with trees alone and just converting to a Context and then to a Function at the end. I have thought about using Context but I don't fully understand what it is supposed to be used for. Does it better optimize out unused operations or scale to having tons of operations? The flexibility and simplicity of trees have made them much more appealing to me so far, mostly due to a lack of understanding on my part, but they are also much less verbose to write and are very similar to shader code.

Also I feel like making a higher level API for things other than shapes, such as combinations or domain ops, could end up being a terrible idea since it would make things less flexible, or an amazing one if you managed to find a perfect solution to cover any conceivable use case (which obviously sounds stupid until you try it and it happens to work).