shader-park / shader-park-website

A virtual 3D sculpture park
MIT License
43 stars 4 forks source link

Create geometry without applying it #60

Closed PWhiddy closed 5 years ago

PWhiddy commented 5 years ago

For example something that allows you to define a composite shape and then use it in a different mode

torinmb commented 5 years ago

yess! it'd be really interesting to have groups. For a simple case is it possible to wrap the code in a function and then push it into an array to call later on in the code?

let spheres = function() {
    color(0, 1, 0);
    displace(0.2, 0, 0);
    sphere(0.2);
    color(1, 0, 0);
    reset();
    displace(-0.2, 0, 0);
    sphere(0.2);
};
let group = [spheres];
group.push(() => {
    reset();
    color(0, 0, 1);
    sphere(0.2);
});
group.forEach(item => item());
torinmb commented 5 years ago

image

yup checks out.

torinmb commented 5 years ago

it'd be nice to abstract that in some way

torinmb commented 5 years ago

It'd also be nice to find a way of preserving the coordinate space and treating the groups as individual objects. That way you could blend between two groups, while the objects inside the group preserve their blend modes

PWhiddy commented 5 years ago

possible api

let sp = createShape(); activateShape(sp); displace(0.2,0.0,0.3); mirrorX(); sphere(0.3); reset(); box(0.2,0.2,0.2); activateShape(); sphere(0.3); difference(); applyShape(sp);

torinmb commented 5 years ago

would something like this be possible?

let sp = createShape(() => {
    displace(0.2,0.0,0.3);
    mirrorX();
    sphere(0.3);
    reset();
    box(0.2,0.2,0.2);
});

sphere(0.3);
difference();
sp();
PWhids commented 5 years ago

Hm that is a functional approach rather than imperative. I like functional in general although is it confusing to use both in the api? Most other high level sdf editors Ive seen (curv, that other one you linked me recently) use a functional approach. Would it be better to switch more entirely to functional rather than the imperative style like processing? for example-

let sub = subtract( () => {
    displace(0.2,0.0,0.3);
    mirrorX();
    sphere(0.3);
    reset();
    box(0.2,0.2,0.2);
});
sphere(0.3);
sub();

This would eliminate all the 'modes' that comes along with the processing api

torinmb commented 5 years ago

If you want to reuse sub you'd only be able to subtract it from other objects though. I think it's important to be able to declaratively create the df and then use the operators on the df. With your above approach, I'd expect subtract to take 2 arguments. Granted it's still combining functional and declarative.

let sp = createShape(() => {
    displace(0.2,0.0,0.3);
    mirrorX();
    sphere(0.3);
    reset();
    box(0.2,0.2,0.2);
});
subtract(sphere(0.2), sp());
PWhids commented 5 years ago

Confused what you mean by "combining functional and declarative". Did you mean imperative?

I totally agree its good to be able to specify a shape and reuse it in different modes. Looking at this again, a cleaner version of the imperative/processing style would probably be like this

startShape();
    displace(0.2,0.0,0.3);
    mirrorX();
    sphere(0.3);
    reset();
    box(0.2,0.2,0.2);
let sp = endShape();

sphere(0.3);
subtract();
applyShape(sp);

Whereas the strictly functional version would look something like this

let sp = shape( () => {
    scene(
        displace(0.2,0.0,0.3, () => {
            mirrorX( () => {
                sphere( 0.3 );
            });
       } ),
       box(0.2,0.2,0.2);
       );
});

scene(
    subtract( 
        sp, 
        sphere(0.3)
    )
);

I agree I think in this case it could be good to combine functional/imperative

torinmb commented 5 years ago

yeah, I think it'd be good to do a combo of functional/imperative. I worry about using purely functional because of endlessly nesting functions, but the ability to chain the operators together while maintaining an order of operations is very appealing.

One drawback to using imperative is that we'll need to use createShape quite often when we're constructing a shape, but the advantage is that the code structure will remain much flatter.

For the imperative example you gave I think it's confusing to capture the shape at endShape. I'd expect to be able to capture the return value on startShape similar to a constructor.

beginShape / endShape will have its own scope, so I'm wondering if we can just use javascript's functions to illustrate that. This would also remove the need to call endShape because the code is encapsulated in a function. You also wouldn't need to call applyShape because you could just call the function that's returned from beginShape / createShape.

I tried to illustrate both below:

// this gets rid of needing to use endShape. 
// I also like this approach because javascript developers would know that the operations that take
// place inside the function are scoped. e.g. the current coordinate space is passed in, but if we 
// call reset() it's only reset inside createShape.
let sp = createShape(() => {
    displace(0.2,0.0,0.3);
    mirrorX();
    sphere(0.3);
    reset();
    box(0.2,0.2,0.2);
});
sphere(0.2);
subtract();
sp();

E.g. calling the return value of createShape

createShape(() => {
    sphere(0.2);
    mixGeo(0.2);
    box(0.2, 0.2, 0.2);
})();
mixGeo(abs(sin(time)));
displace(0.2, 0, 0);
createShape(() => {
    torus(0.4, 0.2);
    subtract();
    box(0.2, 0.2);
})(); 
PWhids commented 5 years ago

That second example would be very confusing to a new user I think. I am also realizing that in order to support nested state inside of a function/lambda, we have to implement full encapsulation of the state with a stack so that if you change any modes (geo modes or color or anything) all that state is pushed/popped from a call stack as you enter the body of each shape. Should be possible in theory. Although will def be a bit tricky and idk how performance will be for complex scenes

torinmb commented 5 years ago

I think it gives us a pretty fun way of encapsulating and chaining together operations

let gird = (width, depth, shape) => {
    return createShape(() => {
        for(let x = 0; x < width; i++) {
            for (let z = 0; z < depth; i++) {
                displace(x, 0, z);
                shape();
            }
        }
    });
}
//for simple shapes you don't need to use createShape
let spheres = grid(20, 20, () => sphere(0.2));
let boxes = grid(20, 20, () => box(0.2, .2, 0.2));

spheres();
displace(0, -0.1, 0);
mixGeo(abs(sin(time)));
boxes();

//or you could do this with a more complex shape
grid(20, 20, createShape(() => {
    sphere(0.2);
    mixGeo(0.4);
    box(0.2, 0.2, 0.2);
}))();
displace(0, -0.1, 0);
mixGeo(abs(sin(time)));
grid(20, 20, () => box(0.2, .2, 0.2))();
PWhids commented 5 years ago

Yeah thats neat. Basically createShape lets you define new primitives which is obviously very useful. There might there be some tricky scenarios where variable scope and ownership might have some problems when sharing variables between nesting levels. Or maybe it would be fine. I'd have to think about it more.

PWhids commented 5 years ago

This is finally implemented!

bnanner/sculpture-park-core@d905a7b2229f1747d2e8c8fa9604819c594f633a