BaubleHelpAbout

Help

underconstruction.gif

First things first: there’s a Discord where you can ask questions, or you’re welcome to use GitHub discussions. Or to shoot me an email. I love talkin’ Bauble. Not that… not that help forums or human interaction are an excuse for bad documentation. That’s definitely not why I’m bringing this up first. why would you even assume

Second things second: Bauble’s autocomplete is Not Half Bad, and all the docs you see here are accessible within the Bauble composer. It should just pop up automatically as you type stuff, but you can press ctrl-space to trigger it manually.

Reference

Shapes

3D shapes

(ball size) source

Returns a 3D shape, which is either a sphere or an ellipsoid, depending on the type of size.

(ball 100)
(ball [50 80 120])

Ellipsoids do not have correct distance fields. Their distance field is only a bound, and it has strange isosurfaces that can make it combine with other shapes oddly:

(ball [30 50 80] | slice y)

(box size [:r round]) source

Returns a 3D shape, a box with corners at (- size) and size. size will be coerced to a vec3.

Think of size like the “radius” of the box: a box with size.x = 50 will be 100 units wide.

(box 100 :r (osc t 3 0 10))
(box [100 (osc t 3 50 100) (oss t 4 50 100)])

(box-frame size thickness [:r round]) source

Returns a 3D shape, the outline of a box.

(union
  (box-frame 100 5 :r (osc t 3 5))
  (box-frame [(osc t 4 30 100) (osc t 5 30 100) (oss t 6 30 100)] 1))

(capsule axis length radius [top-radius]) source

There are two types of capsules: symmetric capsules, which look like pills, or axis-aligned lines:

(capsule y 100 25)

And asymmetric capsules, which have a different radius at the top and bottom:

(capsule y 100 25 10)

(cone axis radius height [:r round]) source

Returns a 3D shape. The height is the extent in only a single direction.

(cone y 50 (sin t * 150) :r (osc t 2 10))

If you supply a rounding factor, the cone will be offset such that it always rests exactly on the zero plane normal to your axis. Is that what you’d expect? I went back on forth on this. I think it’s more intuitive but if you have thoughts I’d like to hear them.

(cube size [:r round]) source

This is an alias for the float overload of box.

(cylinder axis radius height [:r round]) source

Returns a 3D shape, a cylinder oriented along the given axis.

(cylinder y 50 100)

The second argument is twice the length of the cylinder. Like many shapes, you can round it with :r.

(cylinder z 100 50 :r (osc t 3 0 20))

(ellipsoid size) source

Returns a 3D shape. This is an alias for the vec3 overload of ball.

(ground [offset]) source

Returns a 3D plane that only exists while the camera is above it.

This is useful for quickly debugging shadows while still being able to see the underside of your scene, although note that taking the plane away will affect ambient occlusion, so you’re not really seeing the underside.

(line from to from-radius [to-radius]) source

Returns a line between two points.

(line
  [-100 (sin t * 100) (cos t * 100)]
  [100 (cos t * 100) (sin t * 100)]
  10
| union (box-frame 100 1))

You can supply two radii to taper the line over its length:

(line
  [-100 (sin t * 100) (cos t * 100)]
  [100 (cos t * 100) (sin t * 100)]
  (oss t 3 50) (osc t 5 50)
| union (box-frame 100 1))

You can also give 2D points for a line in 2D:

(line
  [-100 (cos t * 100)]
  [100 (sin t * 100)]
  (osc t 3 50) (osc t 5 50))

(octahedron radius [:r round]) source

Returns a 3D shape.

(octahedron 100 :r (sin+ t * 20) | rotate x t y t z t)

(plane normal [offset]) source

Returns a 3D shape that represents the infinite extrusion of a plane in a single direction. normal should be a normalized vector.

Because of its infinite size and featurelessness, this shape is difficult to visualize on its own. You’ll probably want to use it in combination with boolean operations:

(ball 100 | intersect (plane x (sin t * 50)))

(plane x) is the shape that faces the +x direction and extends infinitely in the -x direction, and a positive offset will move it in the +x direction.

r3 source

A 2D shape with zero distance everywhere.

(sphere radius) source

Returns a 3D shape. This is an alias for the float overload of ball.

(torus axis radius thickness) source

Returns a 3D shape, a torus around the provided axis.

(torus z 100 (osc t 3 10 50))

2D shapes

(arc radius angle thickness) source

(arc 100 (osc t 5 tau) (osc t 3 5 20))

(capsule-2d bottom-radius top-radius height) source

(capsule-2d 50 (osc t 3 20 60) (oss t 8 30 100))

(circle radius) source

Returns a 2D shape.

(circle 100)

A circle is the most primitive shape, and it’s very versatile. With no radius it’s a point in space, which can be useful for procedural patterns:

(circle 0
| move (hash2 $i * 50)
| tile: $i [50 50] :oversample true :sample-from -1 :sample-to 1)

(See also worley for an optimized version of that voronoi pattern.)

By varying the radius dynamically, you can produce other interesting shapes:

(circle (ss q.x -100 150 100 150)
| color green)
(circle (sin+ (abs q.y - abs q.x / 10 + (t * 5)) * 20 + 100)
| color sky)

And by projecting it into 3D, you can produce shapes like a disc:

(circle 100 | extrude y | expand 25)

A torus:

(circle 100 | shell | extrude y | expand 25)

A tube:

(circle 100 | shell | extrude z 100 | expand 10)

Among other shapes.

(cut-disk radius bottom) source

Returns a 2D shape.

(cut-disk 100 (sin t * 80))

(hexagon radius [:r round]) source

(hexagon 100 :r (osc t 3 20))

(hexagram radius [:r round]) source

(hexagram 100 :r (osc t 3 20))

(octagon radius [:r round]) source

(octagon 100 :r (osc t 3 20))

(oriented-rect start end width) source

TODOC

(parallelogram size skew) source

Returns a 2D shape. size.x is the width of the top and bottom edges, and size.y is the height of the parellogram.

(parallelogram [80 100] (sin t * 100))

skew is how far the pallorelogram leans in the x direction, so the total width of the prellogram is (size.x + skew) * 2. A skew of 0 gives the same shape as rect.

(pentagon radius [:r round]) source

(pentagon 100 :r (osc t 3 20))

(pie radius angle) source

Returns a 2D shape, something like a pie slice or a pacman depending on angle.

(pie 100 (osc t 5 tau))

(quad-circle radius) source

Returns a 2D shape, an approximation of a circle made out of quadratic bezier curves.

(quad-circle 100)

It’s like a circle, but quaddier.

r2 source

A 2D shape with zero distance everywhere.

(rect size [:r radius]) source

Returns a 2D shape, a rectangle with corners at (- size) and size. size will be coerced to a vec2.

Think of size like the “radius” of the rect: a rect with size.x = 50 will be 100 units wide.

radii can be a single radius or a vec4 of [top-left top-right bottom-right bottom-left].

(union
  (rect 50 | move [-100 100])
  (rect 50 :r 10 | move [100 100])
  (rect 50 :r [0 10 20 30] | move [-100 -100])
  (rect 50 :r [0 30 0 30] | move [100 -100]))

(rhombus size [:r round]) source

Returns a 2D shape. It rhombs with a kite.

(rhombus [100 (osc t 3 50 150)])

(ring radius angle thickness) source

(ring 100 (osc t 5 tau) (osc t 3 5 20))

(star outer-radius inner-radius [:r round]) source

(star 100 70 :r (osc t 3 20))

(trapezoid bottom-width top-width height [:r round]) source

Returns a 2D shape.

(trapezoid (osc t 3 50 100) (oss t 2 100 50) 100)

(triangle & args) source

Usually returns a 2D shape, with various overloads:

(triangle 100)
(triangle [50 100])
(triangle [-50 100] [100 10] [-10 -100])

But it can also return a 3D shape:

(triangle
  [(osc t 4 -100 100) -100 (oss t 5 -100 100)]
  [100 (osc t 6 -100 100) 100]
  [-100 (oss t 7 -100 100) (osc t 8 -100 100)]
| union (box-frame 100 1))

Shape combinators

(bezier shape start control end [:up up] [:from from] [:to to]) source

Returns a 2D or 3D quadratic bezier curve, or extrudes a shape along that curve.

A quadratic bezier curve is defined by three points: a start point, an end point, and a control point.

If you connect a line between the start and the control point, and another line between the control point and the end point, you will have two legs of a triangle. If you then move a point along each line, draw a line between those two points, and then move a point along that line, you will get a quadratic bezier curve.

(def start [-200 (osc (t + 20) 19.1 -200 200)])
(def middle [0 (osc (t + 29) 20.2 -200 200)])
(def end [200 (oss t 21.3 -200 200)])
(def h (sin+ t))

(def v1 (mix start middle h))
(def v2 (mix middle end h))
(union
  (union :r 5
    (line start middle 2)
    (circle 3 | move v1)
  | color red)
  (union :r 5
    (line middle end 2)
    (circle 3 | move v2)
  | color sky)
  (bezier 2 start middle end | color white)
  (union :r 5
    (circle 5 | move (mix v1 v2 h))
    (line v1 v2 2)
  | color magenta))

Maybe better intuition is that you linearly interpolate between two pairs of points, then linearly interpolate between that linear interpolation, you get a quadratic bezier curve.

Like line, bezier curves are defined in 2D or in 3D:

(def start [-200 (osc (t + 20) 19.1 -200 200) (osc (t + 20) 21.5 -100 100)])
(def middle [0 (osc (t + 29) 20.2 -200 200) (osc (t + 29) 19.2 -100 100)])
(def end [200 (oss t 21.3 -200 200) (osc (t + 29) 18.5 -100 100)])
(def h (sin+ t))

(def v1 (mix start middle h))
(def v2 (mix middle end h))
(union
  (union :r 5
    (line start middle 2)
    (ball 3 | move v1)
  | color red)
  (union :r 5
    (line middle end 2)
    (ball 3 | move v2)
  | color sky)
  (bezier 2 start middle end | color white)
  (union :r 5
    (ball 5 | move (mix v1 v2 h))
    (line v1 v2 2)
  | color magenta)
| union (ground -210 | shade dark-gray))

Thes simplest version of a bezier curve produces round lines:

(bezier (osc t 3 1 10) [-100 0] [0 -100] [100 0] | color white)

But you can also pass a shape instead of a float as the first argument, in which case you will extrude the shape along the curve, which you can use to produce differently-shaped lines:

(bezier (rect (osc t 3 1 10)) [-100 0] [0 -100] [100 0] | color white)

Or, in 3D, more interesting curves:

(bezier (torus y 20 (osc t 3 1 10)) [-100 0 100] [0 -100 0] [100 0 -100])

You can also vary the shape over the course of the extrusion, by passing a function as the first argument:

(bezier (fn [$t] (mix 1 10 $t)) [-100 0] [0 -100] [100 0] | color white)

Although the bezier: helper gives you a slightly more convenient way to write this that fits into a pipeline:

(triangle [10 (osc $t 0.1 1 20)]
| bezier: $t [-100 0] [0 -100] [100 0] | color white)

You can also pass :from and :to to constrain the extrusion.

(box 20 | shade red | subtract (ball 23 | shade green) | rotate z ($t * tau)
| bezier: $t [-100 0 100] [0 -100 0] [100 0 -100] :to (osc t 3 0 1))

Like elongate, only a two-dimensional slice of a three-dimensional shape will be extruded along the curve. But if you vary the position of that shape as you extrude it, you can “scan” the entire shape, and produce a stretching effect instead:

(gl/def to (osc t 3 0.1 1))
(box 20 | shade red | subtract (ball 23 | shade green) | rotate z ($t * tau)
| move z (mix -20 20 ($t / to))
| bezier: $t [-100 0 100] [0 -100 0] [100 0 -100] :to to
| slow 0.8)

The relative curve position ($t in this case) is not clamped to from/to, which means that if you’re extruding a 3D shape, you might see unexpected results. For example, this 2D extrusion is a perfect rainbow from red to red:

(circle 5 | radial 6 16 | rotate ($t * tau)
| shade (hsv $t 1 1)
| bezier: $t [-100 0 100] [0 -100 0] [100 0 -100] :to (osc t 3 0 1))

But this similar 3D extrusion is not:

(octahedron 30 | radial z 6 16 | rotate z ($t * tau)
| shade (hsv $t 1 1)
| bezier: $t [-100 0 100] [0 -100 0] [100 0 -100] :to (osc t 3 0 1))

Because the left edge of the shape has a negative $t value. You can explicitly clamp it:

(gl/def to (osc t 3 0 1))
(octahedron 30 | radial z 6 16 | rotate z ($t * tau)
| shade (hsv $t 1 1)
| gl/let [$t (clamp $t 0 to)] _
| bezier: $t [-100 0 100] [0 -100 0] [100 0 -100] :to to)

If that isn’t what you want.

The shape will be oriented along the curve according to the vector :up, which determines what direction will become normal to the curve. The default is +y, but you can specify any normalized vector:

(torus y 30 5
| bezier [-100 0 100] [0 -100 0] [100 0 -100]
  :up y
  :from 0
  :to (sin+ t))
(torus y 30 5
| bezier [-100 0 100] [0 -100 0] [100 0 -100]
  :up z
  :from 0
  :to (sin+ t))
(torus y 30 5
| bezier [-100 0 100] [0 -100 0] [100 0 -100]
  :up x
  :from 0
  :to (sin+ t))

(bezier: shape $t & args) source

Like bezier, but implicitly wraps its first argument in an anonymous function. See bezier for examples.

(extrude shape axis &opt distance) source

Extrude a 2D shape into 3D along the given axis.

distance defaults to 0 and determines the width, length, or height of the final shape. You can also pass inf to get an infinite extrusion (which is slightly cheaper to compute).

(intersect & shapes [:r r] [:s s] [:distance distance] [:color color]) source

Intersect two or more shapes. The named arguments produce a smooth intersection; see union for a thorough description.

(intersect
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

Note that although it doesn’t matter when doing a sharp intersection, you probably want to use :s to smooth over :r, or else the latter shape’s color field will “take over” the earlier shape. Compare:

(intersect :r 30
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))
(intersect :s 30
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

This effect makes sense if you think about the shapes in 2D:

(intersect :r 30
  (circle 100 | move x -50 | color red)
  (circle 100 | move x +50 | color sky))

The second shape was on top of the first shape, so the first shape’s color field is only visible where it fades into the shape of the first. But with a symmetric intersection:

(intersect :s 30
  (circle 100 | move x -50 | color red)
  (circle 100 | move x +50 | color sky))

This doesn’t happen.

(morph shape1 amount shape2 [:distance amount] [:color amount]) source

Morph linearly interpolates between two shapes.

(morph (sin+ t)
  (ball 100 | shade sky)
  (box 100 | shade red))

Concretely this means that it returns a new shape whose individual fields are linear interpolations of the fields on the input shapes.

With an anonymous amount coefficient, both the distance and color fields will be interpolated with the same value. But you can also specify per-field overrides:

(morph (sin+ t) :color (cos+ t)
  (ball 100 | shade sky)
  (box 100 | shade red))

(revolve shape axis &opt offset) source

Revolve a 2D shape around the given axis to return a 3D shape.

(revolve (triangle 100) y)

This lets you create shapes that look like they were turned on a lathe:

(union :r 10
  (circle 40 | move y 80)
  (rect [(ss q.y -90 -52 60 10) 100])
  (rect [(ss q.y -70 57 58 20 * ss q.y -80 -5) 80] | move y -38)
  (rect :r 5 [20 5] | move y 40 | rotate -0.48)
  #| view
  | revolve y)

You can optionally supply an offset to move the shape away from the origin first (the default is 0).

(revolve (triangle 50) y 50)

You can use this to create different types of toroidal shapes:

(revolve (star 30 50 | rotate t) y 100)

(slice shape axis &opt position) source

Take a 2D slice of a 3D shape at a given position along the supplied axis.

position defaults to 0.

(sliced shape axis &opt position) source

Take a 2D slice of a 3D shape at a given position along the supplied axis, and then project it back into 3D space at the same spot.

This is useful for quickly looking inside shapes:

(union
  (box 80 | shade red)
  (ball 100 | shade green)
# try commenting out this line:
| sliced y (sin t * 100)
)

(subtract & shapes [:r r] [:s s] [:distance distance] [:color color]) source

Subtract one or more shapes from a source shape. The named arguments here produce a smooth subtraction, and are similar to the arguments to union.

(subtract
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

Like union and intersect, you can perform a smooth subtraction with :r or :s:

(subtract :r 20
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))
(subtract :s 20
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

See the docs for union and intersect for a full explanation of these arguments and the difference between them.

(union & shapes [:r r] [:s s] [:distance distance] [:color color]) source

Union two or more shapes together. Pass :r or :s to produce a smooth union.

(union
  (ball 100 | shade red | move x -50)
  (ball 100 | shade sky | move x 50))

There are two ways that union (and other boolean operations) can combine color fields. The default is to put later shapes “on top of” earlier shapes:

(union
  (circle 100 | move x -50 | color red)
  (circle 100 | move x +50 | color sky))

And you can perform a smoothed version of this operation with :r:

(union :r 20
  (circle 100 | move x -50 | color red)
  (circle 100 | move x +50 | color sky))

The other way to combine color fields is to simply pick the nearest color. This produces a symmetric color field where the order of arguments doesn’t matter:

(union :s 20
  (circle 100 | move x -50 | color red)
  (circle 100 | move x +50 | color sky))

(You can pass :s 0 if you want a sharp symmetric color union.)

In 3D, the difference is harder to see, because they both produce the same color field at the shape’s surface:

(union
  (union :r 20
    (ball 100 | move x -50 | shade red)
    (ball 100 | move x +50 | shade sky)
  | move y 100)
  (union :s 20
    (ball 100 | move x -50 | shade red)
    (ball 100 | move x +50 | shade sky)
  | move y -100))

But just as in 2D, they produce different colors inside the shapes:

(union
  (union :r 20
    (ball 100 | move x -50 | shade red)
    (ball 100 | move x +50 | shade sky)
  | move y 100)
  (union :s 20
    (ball 100 | move x -50 | shade red)
    (ball 100 | move x +50 | shade sky)
  | move y -100)
| sliced z (sin t * 50))

This is more relevant when using subtract or intersect, which will typically prefer the :s behavior.

You can also pass :distance or :color to specify a different smoothing radius for the separate fields. For example, you can produce a smooth symmetric color union with a sharp distance field:

(union :s 30 :distance 0
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

Or a smooth distance field with a sharp transition in color:

(union :r 30 :color 0
  (ball 100 | move x -50 | shade red)
  (ball 100 | move x +50 | shade sky))

Or any combination like that.

Shape functions

(map-color shape f) source

Apply a function f to the shape’s color field. f should take and return a vec3 expression.

The returned shape has the same dimensions as the input.

This differs from shape/map-color in that the expression is wrapped in gl/let, so you can refer to it multiple times.

(map-distance shape f) source

Apply a function f to the shape’s distance field. f should take and return a :float expression.

The returned shape has the same dimensions as the input.

This differs from shape/map-distance in that the expression is wrapped in gl/let, so you can refer to it multiple times.

(shape/2d distance) source

Returns a new 2D shape with the given distance field.

(shape/3d distance) source

Returns a new 3D shape with the given distance field.

(shape? value) source

Returns true if value is a shape.

Lower-level shape stuff

(shape/color shape) source

Shorthand for (shape/get-field shape :color).

(shape/distance shape) source

Shorthand for (shape/get-field shape :distance).

(shape/get-field shape field) source

Look up a single field on a shape. If the field does not exist, this will return nil.

(shape/map shape f &opt type) source

Alter the fields on a shape, optionally changing its dimension in the process.

f will be called with the value of the field. If you want to do something different for each field, use shape/map-fields.

(shape/map-color shape f) source

Shorthand for (shape/map-field shape :color f).

(shape/map-distance shape f) source

Shorthand for (shape/map-field shape :distance f).

(shape/map-field shape field f) source

Map a single field on a shape. If the field does not exist, this does nothing.

(shape/map-fields shape f &opt type) source

Like shape/map, but f will be called with two arguments: the field name (as a keyword) and its value.

(shape/merge shapes f) source

Merge multiple shapes together. shapes should be a list of shapes that all have the same dimension.

f will be called with an array of all of the fields from each shape, and should return a struct with the fields for the new shape.

merge returns a new shape with the same dimension as its inputs.

(shape/new type & fields) source

Returns a new shape with the given type and fields.

# red circle with radius 10
(shape/new jlsl/type/vec2
  :distance (length q - 10)
  :color [1 0 0])

(shape/shape? value) source

Returns true if value is a shape.

(shape/transplant dest-shape field source-shape) source

Shorthand for (shape/with dest-shape field (shape/get-field source-shape field)).

(shape/type shape) source

Returns the dimension of a shape, as a JLSL type equal to the dimension of a point in the shape – either vec2 or vec3.

(shape/with shape & new-kvs) source

Replace arbitrary fields on a shape.

You probably don’t want to use this. Theoretically shapes in Bauble are collections of arbitrary fields, but in practice :color and :distance are the only fields that are really supported in a meaningful way.

But you could associate other fields with shapes, and use that to model, for example, analytic normals. But none of the existing infrastructure will understand you if you do this.

Transformations

(align target from to) source

Align a shape or a vector to another vector. Both the from and to vectors must have unit length.

This function is useful for “pointing” one shape towards another. For example:

(def pos
  [(sin (t * 1.0) * 100)
   (sin (t * 1.5) * 100)
   (cos (t * 2.0) * 100)])
(union
  (cone y 10 100 | align y (normalize pos))
  (box 10 | move pos))

If from = (- to), the result is undefined: there are infinitely many rotation matrices that reverse a vector’s direction.

(bound shape bounding-shape threshold) source

Wrap an expensive shape with a cheap bounding shape.

This operation evaluates the bounding shape, and if the distance to the bounding shape is less than threshold, it returns that distance. Otherwise it evaluates the real shape.

You can use this to wrap a complicated, expensive shape in a cheaper bounding shape (spheres are best), so that you don’t need to evaluate the expensive shape at every step of the raymarch. This is a very effective optimization if most rays don’t need to enter the bounding shape, for example if you wrap a small shape in a large scene, but it doesn’t really help.

This is hard to visualize because ideally it does not change the render, only makes it faster, but you can see the effect it has on the raymarch by switching to debug convergence view (the magnet icon in the top right).

(box 100 | bound (ball 180) 10)

It’s important that the bounding shape actually contain the inner shape, or you’ll get wild results that will hurt performance as rays fail to converge:

(box 100 | bound (ball 100) 10)

There’s a tradeoff to make with the threshold between increased marching steps and tightening the bound, and a threshold too low may cause weird artifacts when rendering soft shadows or ambient occlusion.

(elongate shape & args) source

Stretch the center of a shape, leaving the sides untouched. The arguments to elongate are similar to move: you pass vectors or axis / magnitude pairs, and their sum will be the total elongation.

(cone y 50 100 | elongate [(osc t 3 50) 0 (osc t 6 100)])
(torus x 50 20 | elongate x (sin+ t * 50) [0 100 0])
(rhombus [100 (gl/if (< q.y 0) 100 50)] | elongate y (osc t 2 0 20))

(expand shape by) source

Expands the provided shape, rounding corners in the process.

This is the same as subtracting by from the distance field. It’s more accurate to say that this “moves between isosurfaces,” so it may not actually round anything if the provided shape is not an exact distance field.

For example, this produces a nicely expanded shape:

(rect 90 | expand (sin+ t * 30))

But this does something weird, because subtraction does not produce an exact distance field:

(rect 90
| subtract (rect 100 | move x 150)
| expand (sin+ t * 30))

(expound shape by &opt magnitude threshold) source

This is a combination of bound and expand, when you want to expand a shape by some expensive expression (e.g. a noise function). Essentially it’s the same as:

(bound
  (expand shape (by * magnitude))
  (expand shape magnitude)
  threshold)

But it produces slightly better code. Consider this:

(ball 100
| expand (perlin+ [(p / 50) t] * osc t 1 20 50))

The rays around the edge of the canvas never approach the shape, but they need to evaluate its distance expression repeatedly until they give up. But 4D perlin noise is pretty expensive to compute, so we can speed up the render by only evaluating it when the current ray is near the shape we’re distorting:

(ball 100
| expound
  (perlin+ [(p / 50) t])
  (osc t 1 20 50))

It’s important that the signal you supply as by not exceed 1, or the bounding shape will be inaccurate.

By default the threshold is equal to the magnitude of the offset, but you can provide a custom threshold to fine-tune the boundary behavior.

If you’re only using procedural distortion to texture a shape, consider using bump for an even larger speedup.

(mirror shape & axes [:r r]) source

Mirror a shape across one or more axes. Normally this takes the absolute value of the coordinates, but if you supply :r it will take a biased square root to give a smooth mirror effect.

(box 50 | rotate x t y t
| move x 50
| mirror x :r (sin t * 20 | max 0))

(move subject & args) source

Translate a shape. You can pass a vector offset:

(move (box 50) [0 (sin t * 100) 0])

Or a vector and a scalar:

(move (box 50) y (sin t * 100))

Which is the same as (move (box 50) (y * 100)).

If you provide multiple vector-scalar pairs, their sum is the final offset:

(move (box 50)
  x (sin t * 100)
  y (cos t * 100)
  -z (sin t * 100))

move can take a shape, a vector, or a camera.

If you vary the amount of movement by the current position in space, you can distort shapes in various ways:

(box 100 | move x (sin (p.y / 100 * pi) * 30))
(cylinder y 100 10
| move y (atan p.x p.z * 10 | sin * (length p | ss 10 100 0 10)))
(box [100 10 100] | move y (p.xz / 20 | pow 2 | sum) | slow 0.5)

(pivot (operation subject & args) point) source

Apply a transformation with a different pivot point. You can combine this with any operation, but it’s probably most useful with rotate and scale.

This is a syntactic transformation, so it requires a particular kind of invocation. It’s designed to fit into a pipeline, immediately after the operation you want to apply:

# rotate around one corner
(rect 50 | rotate t | pivot [50 50])

This essentially rewrites its argument to:

(gl/let [$pivot [50 50]]
  (rect 50 | move (- $pivot) | rotate t | move $pivot))

(rotate subject & args) source

Rotate a shape or a vector. Positive angles are counter-clockwise rotations.

In 3D, the arguments should be pairs of axis angle. For example:

(rotate (box 100) x t y (sin t))

All axis arguments must be unit vectors. There are built-in axis variables x/+y/-z for the cardinal directions, and these produce optimized rotation matrices. But you can rotate around an arbitrary axis:

(rotate (box 100) [1 1 1 | normalize] t)

The order of the arguments is significant, as rotations are not commutative.

The first argument to rotate can be a shape, vector, or camera.

In 2D, the arguments should just be angles; no axis is allowed.

You can use rotate to make lots of cool effects. By varying the angle of rotation, you can create twists:

(box [50 100 50]
| rotate y (p.y / 100 * (cos+ t)))

Twirls:

(box [100 50 100]
| rotate y (length p.xz / 50 * (cos+ t)))

And bends:

(box [50 100 100]
| rotate y (p.z / 100 * (cos+ t)))

Or any number of other cool effects!

(box [50 100 50]
| rotate y (sin (p.y / 10) * sin t * 0.2))

(scale shape & args) source

Scale a shape. If the scale factor is a float, this will produce an exact distance field.

(rect 50 | scale 2)

If the scale factor is a vector, space will be distorted by the smallest component of that vector, and produce an approximate distance field:

(rect 50 | scale [2 1])

With an even number of arguments, scale expects axis amount pairs. Unlike rotate, it won’t work with arbitrary axes – you must give it a cardinal axis.

(rect 50 | scale x 0.5 y 2)

(shell shape &opt thickness) source

Returns a hollow version of the provided shape (the absolute value of the distance field).

(circle 100 | shell 5)

In 3D, it’s hard to see the effect without cutting into the result:

(ball 100 | shell 5 | intersect (plane x (osc t 3 0 100)))

(slow shape amount) source

Scales the shape’s distance field, causing the raymarcher to converge more slowly. This is useful for raymarching distance fields that vary based on p – shapes that don’t actually provide an accurate distance field unless you are very close to their surfaces. Compare the following examples, with and without slow:

(box 100
| rotate y (p.y / 30)
| rotate x t)
(box 100
| rotate y (p.y / 30)
| rotate x t
| slow 0.5)

Note however that slow will also affect the behavior of anything that depends on a shape’s distance field, such as smooth boolean operations, morphs, soft shadows, and so on. A future version of Bauble may mitigate these effects, but it is the way that it is right now.

# slowing the distance field introduces asymmetry
# into the smooth union
(union :r 50
  (ball 100 | move x 75)
  (ball 100 | move x -75 | slow (osc t 4 0.25 1)))

Dynamic variables

depth source

The distance that the current ray has marched, equal to (distance ray-origin P). Not defined in 2D.

dist source

(Color only!) The value of the global distance field at P. In 3D, this should be a very small positive number, assuming the ray was able to converge correctly. In 2D, this gives a more useful value.

frag-coord source

The logical position of the current fragment being rendered, in the approximate range -0.5 to 0.5, with [0 0] as the center of the screen. Note though that we always shade pixel centers, so we never actual render -0.5 or 0.5, just nearby subpixel approximations depending on the antialiasing level.

This is equal to (Frag-Coord - (resolution * 0.5) / max resolution).

Frag-Coord source

The center of the current pixel being rendered. Pixel centers are at [0.5 0.5], so with no anti-aliasing this will have values like [0.5 0.5], [1.5 0.5], etc. If you’re using multisampled antialiasing, this will have off-centered values like [0.3333 0.3333].

gradient source

(Color only!) An approximation of the 2D distance field gradient at Q.

normal source

(Color only!) A normalized vector that approximates the 3D distance field gradient at P (in other words, the surface normal for shading).

p source

The local point in 3D space. This is the position of the current ray, with any transformations applied to it.

P source

The global point in 3D space. This is the position of the current ray before any transformations are applied to it.

q source

The local point in 2D space. This is the position being shaded, with any transformations applied.

Q source

The global point in 2D space.

ray source

The current ray being used to march and shade the current fragment. This always represents the ray from the camera, even when raymarching for shadow casting.

A ray has two components: an origin and a direction. origin is a point in the global coordinate space, and you can intuitively think of it as “the location of the camera” when you’re using the default perspective camera (orthographic cameras shoot rays from different origins).

The direction is always normalized.

resolution source

The size, in physical pixels, of the canvas being rendered. In quad view, this will be smaller than the physical size of the canvas.

t source

The current time in seconds.

viewport source

You don’t have to think about this value unless you’re implementing a custom main function, which you probably aren’t doing.

This represents the portion of the canvas currently being rendered. The xy components are the start (bottom left) and the zw coordinates are the size.

Normally this will be equal to [[0 0] resolution], but when rendering quad-view or a chunked render, it may have a different origin or resolution.

You can use (gl-frag-coord.xy - viewport.xy) in order to get the logical fragment position (the value exposed to a typical shader as Frag-Coord).

Bauble variables

aa-grid-size source

The size of the grid used to sample a single pixel. The total samples per pixel will be the square of this number. The default value is 1 (no anti-aliasing).

background-color source

A variable that determines the background color of the canvas.

Default is graydient. This can be a vec3 or a vec4:

(ball 100)
(set background-color transparent)

camera source

An expression for a ray that determines the position and direction of the camera.

default-2d-color source

A variable that determines the default color to use when rendering a 2D shape with no color field.

Default is isolines.

default-3d-color source

A variable that determines the default color to use when rendering a 3D shape with no color field.

Default is (mix normal+ [1 1 1] (fresnel 5)).

subject source

A variable that determines what Bauble will render.

You can set this variable explicitly to change your focus, or use the view macro to change your focus. If you don’t set a subject, Bauble will render the last shape in your script.

(view subject) source

A shorthand for (set subject _) that fits nicely into pipe notation, e.g. (ball 50 | view).

Shading

*lights* source

The default lights used by the shade function. You can manipulate this using setdyn or with-dyns like any other dynamic variable, but there is a dedicated with-lights function to set it in a way that fits nicely into a pipeline.

(blinn-phong light color [:s shininess] [:g glossiness]) source

A Blinn-Phong shader, intended to be passed as an argument to shade. :s controls the strength of specular highlights, and :g controls the glossiness.

(ball 100 | shade :f blinn-phong [1 0 0] :s 1 :g (osc t 5 5 30))

(bump shape by [amount]) source

Alter the normal for a shape. You can use this along with noise expressions to give the appearance of texture without the expense of evaluating the offset multiple times during the march.

Compare, an actually-bumpy shape:

(ball 100 | shade red | expand (perlin (p / 10)))

To a shape with normals inspired by that bumpiness:

(ball 100 | shade red | bump (perlin (p / 10)) 0.3)

This is much cheaper than using expand, so if you’re only trying to add a little texture and don’t need to change the shape, consider using bump instead.

(If you really do care about distorting the geometry, see expound for a more efficient way to do that.)

The expression to bump will be evaluated using calculate-normal, so it should vary with p. This is a much cheaper way to add texture than trying to sculpt it into the distance field. Orange peel:

(ball 100 | shade (hsv 0.05 1 0.75) | bump (perlin+ p) 0.2)
(set camera (camera/perspective [(cos (t / 2)) 0 (sin (t / 2)) * 200] | camera/zoom (osc t 7 1 2)))

(calculate-gradient expr) source

Evaluates the given 2D distance expression four times, and returns an approximation of the expression’s gradient.

(calculate-normal expr) source

Evaluates the given 3D distance expression four times, and returns an approximation of the expression’s gradient.

(cast-light-hard-shadow light-color light-position) source

TODOC

(cast-light-no-shadow light-color light-position) source

TODOC

(cast-light-soft-shadow light-color light-position softness) source

TODOC

(color shape color) source

Set a shape’s color field. This is the primitive surfacing operation, both in 2D:

(circle 100 | color [1 0.5 0.5])

And in 3D:

(box 100 :r 10 | color [1 0.5 0.5])

Although you will typically set a color field to a dynamic expression:

(box 100 :r 10
| color (hsv (atan2 p.xy / tau) 1 1
  * dot normal [1 2 3 | normalize]))

You can also pass another shape, in which case the color field will be copied to the destination shape:

(box 100 :r 10 | color
  (union (box 100 | shade [0 1 0]) (ball 125 | shade [1 0 0])))

(fresnel [exponent]) source

Returns an approximate fresnel intensity. exponent defaults to 5.

(ball 100
| shade [1 0.5 0.5]
| tint [1 1 1] (fresnel (osc t 5 0.5 5)))

graydient source

The default background color, a gray gradient.

isolines source

A color that represents the visualization of the 2D gradient. This is the default color used when rendering a 2D shape with no color field.

(Light color direction brightness) source

(light/ambient color [offset] [:brightness brightness] [:hoist hoist]) source

Shorthand for (light/point color (P + offset)).

With no offset, the ambient light will be completely directionless, so it won’t contribute to specular highlights. By offsetting by a multiple of the surface normal, or by the surface normal plus some constant, you can create an ambient light with specular highlights, which provides some depth in areas of your scene that are in full shadow.

(light/directional color dir dist [:shadow softness] [:brightness brightness] [:hoist hoist]) source

A light that hits every point at the same angle.

Shorthand for (light/point color (P - (dir * dist))).

(light/map light f) source

f takes and returns a Light expression.

(light/map-brightness light f) source

f takes and returns a :float expression.

(light/map-color light f) source

f takes and returns a vec3 expression.

(light/point color position [:shadow softness] [:brightness brightness] [:hoist hoist]) source

Returns a new light, which can be used as an input to some shading functions.

Although this is called a point light, the location of the “point” can vary with a dynamic expression. A light that casts no shadows and is located at P (no matter where P is) is an ambient light. A light that is always located at a fixed offset from P is a directional light.

By default lights don’t cast shadows, but you can change that by passing a :shadow argument. 0 will cast hard shadows, and any other expression will cast a soft shadow (it should be a number roughly in the range 0 to 1).

Shadow casting affects the brightness of the light. You can also specify a baseline :brightness explicitly, which defaults to 1.

Shadow casting always occurs in the global coordinate space, so you should position lights relative to P, not p.

By default light calculations are hoisted (see gl/def for more info). This is an optimization that’s helpful if you have a light that casts shadows that applies to multiple shaded surfaces that have been combined with a smooth union or morph or other shape combinator. Instead of computing shadows twice and mixing them together, the shadow calculation will be computed once at the top level of the shader. Note though that this will prevent you from referring to variables that don’t exist at the top level – e.g. anything defined with gl/let, or the index argument of tiled shape. If you want to make a light that dynamically varies, pass :hoist false.

(light? value) source

Returns true if value is a GLSL expression with type Light.

normal+ source

A color that represents the visualization of the 3D normal. This is the default color used when rendering a 3D shape with no color field.

(occlusion [:steps step-count] [:dist dist] [:dir dir] [:hoist hoist]) source

Approximate ambient occlusion by sampling the distance field at :steps positions (default 8) linearly spaced from 0 to :dist (default 20). The result will range from 1 (completely unoccluded) to 0 (fully occluded).

By default the occlusion samples will be taken along the surface normal of the point being shaded, but you can pass a custom :dir expression to change that. You can use this to e.g. add jitter to the sample direction, which can help to improve the quality.

Occlusion is somewhat expensive to calculate, so by default the result will be hoisted, so that it’s only calculated once per iteration (without you having to explicitly gl/def the result). However, this means that the occlusion calculation won’t take into account local normal adjustments, so you might want to pass :hoist false.

(shade shape & args [:f f] :& kargs) source

shade colors a shape with a map-reduce over the current lights. It’s a higher-order operation that takes a shading function (by default blinn-phong) – and calls it once for every light in *lights*.

(ball 100 | shade [1 0.25 0.5])

All arguments to shade will be passed to the shading function, and only evaluated once, no matter how many lights there are. See the documentation for blinn-phong for a description of the arguments it takes.

If you define a custom shading function, its first argument should be a light:

(ball 100 | shade [1 0.25 0.5] :f (fn [light color]
  (dot normal light.direction * light.brightness
  | quantize 5 * light.color * color
)))

A light is a struct with three fields: color, direction, and brightness. brightness is roughly “shadow intensity,” and in the default lights it includes an ambient occlusion component. This shader hardens the edges of shadows with step, and uses it to apply a custom shadow color:

(torus x 10 5 | rotate z t | radial y 20 100 | union (ground -120)
| shade [0.5 1.0 1.0] :f (fn [light color]
  (dot normal light.direction * light.color * (mix [0.5 0 0] color (step 0.5 light.brightness))
  )))

Why does a light give you a direction instead of a position? direction is a little more robust because you don’t have to remember to special-case the zero vector (ambient lights), and theoretically if you want to do anything position-dependent you should reflect that in the color or brightness. But maybe it should give you both. I’m torn now.

(tint shape color &opt amount) source

Add a color to a shape’s color field.

(ball 100 | shade normal+ | tint [1 0 0] (sin+ t))

(union-color & shapes [:r r] [:s s]) source

union-color is like union, but it only affects color fields: it returns a shape with the same distance field as its first argument.

You can use it to “paint” or “stamp” shapes, in 2D or 3D. For example:

(star 100 50 | color sky | union-color (circle 60 | color orange))
(ball 100
| shade sky
# change this to a union
| union-color (star 50 30 | color red | extrude z inf | radial y 5 | rotate y t))

(with-lights shape & lights) source

Evaluate shape with the *lights* dynamic variable set to the provided lights.

The argument order makes it easy to stick this in a pipeline. For example:

(ball 100
| shade [1 0 0]
| with-lights
  (light/point 0.5 [100 100 0])
  (light/ambient 0.5))

Repetition

(radial shape [axis] count [offset] [:oversample oversample] [:sample-from sample-from] [:sample-to sample-to]) source

Repeat an angular slice of space count times around the given axis.

(torus x 100 1 | radial y 24)

With an offset argument, you can translate the shape away from the origin first:

(torus x 100 1 | radial y 24 (osc t 5 0 120))

If you’re repeating a shape that is not symmetric, you can use :oversample true to evaluate multiple instances at each pass, essentially considering the distance not only to this slice, but also to neighboring slices. Compare these two distance fields:

(triangle [50 100] | radial 12 100)
(triangle [50 100] | radial 12 100 :oversample true)

The default oversampling is :sample-from 0 :sample-to 1, which means looking at one adjacent slice, asymmetrically based on the location of the point (so when evaluating a point near the right edge of a slice, it will look at the slice to the right, but not the slice to the left). By passing :sample-from -1, you can also look at the “far” slice. By passing :sample-from 0 :sample-to 2, you can look at two slices in the direction of the nearest slice.

This can be useful when raymarching a 3D space where each slice produces a different shape, or where the shape you’re marching doesn’t fit into a single slice. For example:

(cone y 25 100 :r 1
| radial z 12 100 :oversample true :sample-from -1)

(radial* [axis] count [offset] get-shape [:oversample oversample] [:sample-from sample-from] [:sample-to sample-to]) source

Like radial, but the shape is a result of invoking get-shape with one argument, a GLSL variable referring to the current slice of space.

(radial* z 12 100 (fn [$i]
  (ball 50
  | color (hsv (hash $i) 0.5 1))))

You can use this to generate different shapes or colors at every sampled slice of space. The index will be a float with integral components that represents the current slice being considered.

See also radial:, which is a more convenient macro version of this function.

(radial: shape $i & args) source

Like radial*, but its first argument should be a form that will become the body of the function. Basically, it’s a way to create a repeated shape where each instance of the shape varies, and it’s written in a way that makes it conveniently fit into a pipeline:

(ball 50
| color (hsv (hash $i) 0.5 1)
| radial: $i z 12 100)

(tile shape size [:limit limit] [:oversample oversample] [:sample-from sample-from] [:sample-to sample-to]) source

Repeat the region of space size units around the origin. Pass :limit to constrain the number of repetitions. See tile: or tile* if you want to produce a shape that varies as it repeats.

To repeat space only along some axes, pass 0. For example, to only tile in the y direction:

(tile (ball 50) [0 100 0])

If you’re repeating a shape that is not symmetric, you can use :oversample true to evaluate multiple instances at each pass, essentially considering the distance not only to this tile, but also to neighboring tiles. Compare these two distance fields:

(rect 30 | rotate 0.3 | tile [80 80] :oversample false)
(rect 30 | rotate 0.3 | tile [80 80] :oversample true)

The default oversampling is :sample-from 0 :sample-to 1, which means looking at one adjacent tile, asymmetrically based on the location of the point (so when evaluating a point near the right edge of a tile, it will look at the adjacent tile to the right, but not the tile to the left). By passing :sample-from -1, you can also look at the tile to the left. By passing :sample-from 0 :sample-to [2 1 1], it will look at two tiles to the right in the x direction, and one tile up/down/in/out.

This can be useful when raymarching a 3D space where each tile is quite different, but note that it’s very costly to increase these values. If you’re tiling a 3D shape in all directions, the default :oversample parameters will do 8 distance field evaluations; :sample-from -1 :sample-to 1 will do 27.

(tile* size get-shape [:limit limit] [:oversample oversample] [:sample-from sample-from] [:sample-to sample-to]) source

Like tile, but the shape is a result of invoking get-shape with one argument, a GLSL variable referring to the current index in space. Unlike tile, size must be a vector that determines the dimension of the index variable.

(tile* [10 10] (fn [$i] 
  (circle 5 
  | color (hsv (hash $i) 0.5 1))))

You can use this to generate different shapes or colors at every sampled tile. The index will be a vector with integral components that represents the current tile being evaluated. So in 3D, the shape at the origin has an index of [0 0 0] and the shape above it has an index of [0 1 0].

See also tile:, which is a more convenient macro version of this function.

(tile: shape $i & args) source

Like tile*, but its first argument should be a form that will become the body of the function. Basically, it’s a way to create a repeated shape where each instance of the shape varies, and it’s written in a way that makes it conveniently fit into a pipeline:

(circle 5 
| color (hsv (hash $i) 0.5 1) 
| tile: $i [10 10])

Noise

(fbm octaves noise point [period] [:f f] [:gain gain]) source

Run the given noise function over the input multiple times, with different periods, and sum the results with some decay.

You can use this to create interesting procedural patterns out of simple noise functions. For example, perlin noise is a fairly smooth and “low resoultion” pattern:

(color r2 (vec3 (perlin q 30 | remap+)))

But by summing multiple instances of perlin noise sampled with different periods, we can create more detailed patterns:

(color r2 (vec3 (fbm 4 perlin q 30 | remap+)))
(color r2 (vec3 (fbm 4 perlin q 30 :gain 0.8 | remap+)))

You can use this to create many effects, like clouds or landscapes, although note that it is pretty expensive to use in a distance field. For example, 2D perlin noise makes a pretty good landscape:

(plane y
| expound (fbm 7 perlin p.xz 100 | remap+) 50 10
| intersect (ball [200 100 200])
| slow 0.5
| shade normal+ :g 20)

3D perlin noise is more expensive, but produces a detailed rocky effect:

(plane y
| expound (fbm 7 perlin p 50 | remap+) 50 10
| intersect (ball [200 100 200])
| slow 0.5
| shade normal+ :g 20)

By default the function will be evaluated with its input multiplied by two every time. You can change this by passing a different value as :f, or you can pass a function to provide a custom transformation:

(color r2 (vec3 (fbm 4
  (fn [q] (sin q.x + cos q.y /))
  :f (fn [q] (rotate (q * 2) pi/4 (t / 20)))
  q 20 | remap+)))

(hash & args) source

Return a pseudorandom float. The input can be a float or vector. With multiple arguments, this will return the hash of the sum.

This should return consistent results across GPUs, unlike high-frequency sine functions.

(hash2 & args) source

Return a pseudorandom vec2. The input can be a float or vector. With multiple arguments, this will return the hash of the sum.

This should return consistent results across GPUs, unlike high-frequency sine functions.

(hash3 & args) source

Return a pseudorandom vec3. The input can be a float or vector. With multiple arguments, this will return the hash of the sum.

This should return consistent results across GPUs, unlike high-frequency sine functions.

(hash4 & args) source

Return a pseudorandom vec4. The input can be a float or vector. With multiple arguments, this will return the hash of the sum.

This should return consistent results across GPUs, unlike high-frequency sine functions.

(perlin point &opt period) source

Returns perlin noise ranging from -1 to 1. The input point can be a vector of any dimension.

Use perlin+ to return noise in the range 0 to 1.

(perlin+ point &opt period) source

Perlin noise in the range 0 to 1.

(ball 100 | color (perlin+ (p.xy / 10) | vec3))
(ball 100 | color (perlin+ (p / 10) | vec3))
(ball 100 | color (perlin+ [(p / 10) t] | vec3))

(simplex point &opt period) source

Returns simplex noise ranging from -1 to 1. The input point can be a vector of any dimension.

Use simplex+ to return noise in the range 0 to 1.

(simplex+ point &opt period) source

simplex noise in the range 0 to 1.

(ball 100 | color (simplex+ (p.xy / 10) | vec3))
(ball 100 | color (simplex+ (p / 10) | vec3))
(ball 100 | color (simplex+ [(p / 10) t] | vec3))

(worley point &opt period) source

Worley noise, also called cellular noise or voronoi noise. The input point can be a vec2 or a vec3.

(ball 100 | color (worley (p.xy / 30) | vec3))
(ball 100 | color (worley (p / 30) | vec3))

Returns the nearest distance to points distributed randomly within the tiles of a square or cubic grid.

(worley2 point &opt period) source

Like worley, but returns the nearest distance in x and the second-nearest distance in y.

(ball 100 | color [(worley2 (p.xy / 30)) 1])
(ball 100 | color [(worley2 (p / 30)) 1])

Camera

(camera/dolly camera amount) source

Move the camera forward or backward.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/dolly (sin t * 100)))

Useful for Hitchcocking:

(morph (ball 50) (box 50) 2
| union
  (circle 200 | extrude y 10 | move y -100)
  (box [100 200 50] | tile [300 0 300] :limit 4 | move [0 0 -1000])
| shade (vec3 0.75))

(set camera (camera/perspective [0 100 600] :fov 45
| camera/dolly (sin+ t * -500)
| camera/zoom (sin+ t + 1)
))

(camera/orthographic position [:target target] [:dir dir] [:roll roll] [:scale scale]) source

Returns a camera with an orthographic projection and the given :scale (default 512). Other arguments are the same as camera/perspective.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(def pos [(sin t * 200) (cos+ (t / 2) * 300) 500])
(set camera (camera/orthographic pos))

An orthographic camera shoots every ray in the same direction, but from a different origin. In effect it produces an image without any sense of depth, because objects farther away from the camera don’t get smaller. Compare the following scenes, with a typical perspective camera:

(box 50 | tile [150 0 150])
(set camera (camera/perspective [1 1 1 | normalize * 512]))

And the same scene with an orthographic camera:

(box 50 | tile [150 0 150])
(set camera (camera/orthographic [1 1 1 | normalize * 512]))

(camera/pan camera angle [:up up]) source

Rotate the camera left and right.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/pan (sin t * 0.2)))

By default this rotation is relative to the camera’s current orientation, so the image you see will always appear to be moving horizontally during a pan. But you can provide an absolute :up vector to ignore the camera’s roll. (I think the difference is easier to understand if you unroll the camera afterward.)

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/roll pi/4
| camera/pan (sin t * 0.2)
# | camera/roll -pi/4
))
(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/roll pi/4
| camera/pan (sin t * 0.2) :up y
# | camera/roll -pi/4
))

(camera/perspective position [:target target] [:dir dir] [:roll roll] [:fov fov]) source

Returns a camera with a perspective projection located at position and pointing towards the origin. You can have the camera face another point by passing :target, or set the orientation explicitly by passing a normalized vector as :dir (you can’t pass both).

You can change the field of view by passing :fov with a number of degrees. The default is 60, and the default orbiting free camera uses 45.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(def pos [(sin t * 200) (cos+ (t / 2) * 300) 500])
(set camera (camera/perspective pos :fov 45))

(camera/ray camera) source

Returns the perspective-adjusted ray from this camera for the current frag-coord. You probably don’t need to call this.

(camera/roll camera angle) source

Roll the camera around.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/roll (sin t * 0.2)))

(camera/tilt camera angle [:up up]) source

Rotate the camera up and down.

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/tilt (sin t * 0.2)))

As with pan, you can supply an absolute :up vector to use instead of the camera’s current roll.

(camera/zoom camera amount) source

Zoom the camera by changing its field of view (for a perspective camera) or scale (for an orthographic camera).

(morph (ball 50) (box 50) 2
| union (circle 200 | extrude y 10 | move y -100)
| shade (vec3 0.75))
(set camera (camera/perspective [0 100 600] :fov 45
| camera/zoom (sin t * 0.2 + 1)))

(camera? value) source

Returns true if value is a GLSL expression with type PerspectiveCamera or OrthographicCamera.

(OrthographicCamera position direction up scale) source

(perspective-vector fov) source

Returns a unit vector pointing in the +z direction for the given camera field-of-view (in degrees).

(PerspectiveCamera position direction up fov) source

(Ray origin direction) source

(ray? value) source

Returns true if value is a GLSL expression with type Ray.

Colors

black source

(set background-color black)
(ball 100 | shade black)

blue source

(set background-color blue)
(ball 100 | shade blue)

cyan source

(set background-color cyan)
(ball 100 | shade cyan)

dark-gray source

(set background-color dark-gray)
(ball 100 | shade dark-gray)

gray source

(set background-color gray)
(ball 100 | shade gray)

green source

(set background-color green)
(ball 100 | shade green)

hot-pink source

(set background-color hot-pink)
(ball 100 | shade hot-pink)

(hsl hue saturation lightness) source

Returns a color.

(hsv hue saturation value) source

Returns a color.

light-gray source

(set background-color light-gray)
(ball 100 | shade light-gray)

lime source

(set background-color lime)
(ball 100 | shade lime)

magenta source

(set background-color magenta)
(ball 100 | shade magenta)

(ok/hcl hue chroma lightness) source

This is a way to generate colors in the Oklab color space.

Oklab colors maintain “perceptual brightness” better than hsv or hsl:

(union 
  (rect [200 50] | color (hsv    (q.x / 200 | remap+) 1 1) | move y 51)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) 0.5 0.5) | move y -51))

Note that there is no yellow in that rainbow, because yellow is a bright color. If we increase the lightness above 0.5, we notice that pure blue disappears:

(union 
  (rect [200 50] | color (hsv    (q.x / 200 | remap+) 1 1) | move y 51)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) 0.5 1) | move y -51))

Because pure blue is a dark color.

chroma is analogous to “saturation,” and should approximately range from 0 to 0.5.

(union 
  (rect [200 50] | color (hsv    (q.x / 200 | remap+) (q.y / 50 | remap+) 1) | move y 101)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) (q.y / 50 | remap+) 1) | move y 0)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) (q.y / 50 | remap+) 0.5) | move y -101))

lightness should approximately range from 0 to 1, but is not properly defined at all hues or chromas. For example, if we try to make a high-chroma yellow too dark, it slips into being green instead:

(union 
  (rect [200 50] | color (hsv    (q.x / 200 | remap+) 1 (q.y / 50 | remap+)) | move y 151)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) 0.25 (q.y / 50 | remap+)) | move y 50)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) 0.5 (q.y / 50 | remap+)) | move y -51)
  (rect [200 50] | color (ok/hcl (q.x / 200 | remap+) 0.75 (q.y / 50 | remap+)) | move y -151))

(ok/mix from to by) source

Linearly interpolate between two RGB colors using the Oklab color space. This is the same as converting them to the Oklab color space, mixing them, and then converting back to RGB, but it’s more efficient.

(union 
  (rect [200 50] | color (ok/mix red blue (q.x / 200 | remap+)) | move y 50)
  (rect [200 50] | color (mix    red blue (q.x / 200 | remap+)) | move y -50))

(ok/of-rgb rgb) source

Convert an Oklab color to a linear RGB color. You can use this, along with ok/to-rgb, to perform color blending in the Oklab color space.

In these examples, Oklab is on the left, and linear RGB mixing is on the right:

(union
  (morph (osc t 10 | ss 0.01 0.99)
    (ball 50 | shade yellow | map-color ok/of-rgb)
    (box 50 | shade blue | map-color ok/of-rgb)
  | map-color ok/to-rgb
  | move [-60 0 60])
  (morph (osc t 10 | ss 0.01 0.99)
    (ball 50 | shade yellow)
    (box 50 | shade blue)
  | move [60 0 -60]))
(union
(union :r (osc t 5 0 30)
  (box 50 | shade red | map-color ok/of-rgb)
  (ball 40 | shade blue | map-color ok/of-rgb | move y 50)
| map-color ok/to-rgb
| move [-60 0 60])
(union :r (osc t 5 0 30)
  (box 50 | shade red)
  (ball 40 | shade blue | move y 50)
| move [60 0 -60]))

(ok/to-rgb ok) source

Convert a linear RGB color to the Oklab color space. See ok/of-rgb for examples.

orange source

(set background-color orange)
(ball 100 | shade orange)

purple source

(set background-color purple)
(ball 100 | shade purple)

red source

(set background-color red)
(ball 100 | shade red)

sky source

(set background-color sky)
(ball 100 | shade sky)

teal source

(set background-color teal)
(ball 100 | shade teal)

transparent source

This is a vec4, not a vec3, so you can basically only use it as a background color.

(set background-color transparent)

white source

(set background-color white)
(ball 100 | shade white)

yellow source

(set background-color yellow)
(ball 100 | shade yellow)

Rotation

(alignment-matrix from to) source

Return a 3D rotation matrix that aligns one normalized vector to another.

Both input vectors must have a unit length!

If from = (- to), the result is undefined.

(rotation-around axis angle) source

A rotation matrix about an arbitrary axis. More expensive to compute than the axis-aligned rotation matrices.

(rotation-matrix & args) source

Return a rotation matrix. Takes the same arguments as rotate, minus the initial thing to rotate.

(rotation-x angle) source

A rotation matrix about the X axis.

(rotation-y angle) source

A rotation matrix about the Y axis.

(rotation-z angle) source

A rotation matrix about the Z axis.

GLSL helpers

(gl/def name expression) source

You can use gl/def to create new top-level GLSL variables which will only be evaluated once (per distance and color field evaluation). This is useful in order to re-use an expensive value in multiple places, when that value only depends on values that are available at the beginning of shading.

(gl/def signal (perlin+ (p / 20)))
(shape/3d (signal * 0.5)
| intersect (ball 100)
| color [signal (pow signal 2) 0])

This is shorthand for (def foo (hoist expression "foo")).

Note that since the signal is evaluated at the top-level, p will always be the same as P. Consider this example:

(gl/def signal (perlin+ (p / 20)))
(shape/3d (signal * 0.5)
| intersect (ball 100)
| color [signal (pow signal 2) 0]
| move x (sin t * 100)
)

Change the gl/def to a regular def to see some of the impliciations of hoisting a computation.

(gl/defn return-type name params & body) source

Defines a GLSL function. You must explicitly annotate the return type and the type of all arguments. The body of the function uses the GLSL DSL, i.e. it is not normal Janet code.

(gl/defn :vec3 hsv [:float hue :float saturation :float value]
  (var c (hue * 6 + [0 4 2] | mod 6 - 3 | abs))
  (return (value * (mix (vec3 1) (c - 1 | clamp 0 1) saturation))))

(gl/do & body) source

Execute a series of GLSL statements and return the final expression.

(ball 100 | color 
  (gl/do "optional-label"
    (var c [1 0 1])
    (for (var i 0:u) (< i 10:u) (++ i)
      (+= c.g 0.01))
    c))

The body of this macro is not regular Janet code, but a special DSL that is not really documented anywhere, making it pretty hard to use.

(gl/hoist expression &opt name) source

Return a hoisted version of the expression See the documentation for gl/def for an explanation of this.

(gl/if condition then else) source

A GLSL ternary conditional expression.

(ball 100 | color 
  (gl/if (< normal.y 0) 
    [1 0 0] 
    [1 1 0]))

(gl/iife & body) source

Like gl/do, except that you can explicitly return early.

(ball 100 | color
  (gl/iife "optional-label"
    (var c [1 0 1])
    (if (< normal.y 0)
      (return c))
    (for (var i 0:u) (< i 10:u) (++ i)
      (+= c.g (p.x / 100 / 10)))
    c))

(gl/let bindings & body) source

Like let, but creates GLSL bindings instead of a Janet bindings. You can use this to reference an expression multiple times while only evaluating it once in the resulting shader.

For example:

(let [s (sin t)]
  (+ s s))

Produces GLSL code like this:

sin(t) + sin(t)

Because s refers to the GLSL expression (sin t).

Meanwhile:

(gl/let [s (sin t)]
  (+ s s))

Produces GLSL code like this:

float let(float s) {
  return s + s;
}

let(sin(t))

Or something equivalent. Note that the variable is hoisted into an immediately-invoked function because it’s the only way to introduce a new identifier in a GLSL expression context.

You can also use Bauble’s underscore notation to fit this into a pipeline:

(s + s | gl/let [s (sin t)] _)

If the body of the gl/let returns a shape, the bound variable will be available in all of its fields. If you want to refer to variables or expressions that are only available in some fields, pass a keyword as the first argument:

(gl/let :color [banding (dist * 10)]
  (box 100 | shade [1 banding 0]))

(gl/with bindings & body) source

Like gl/let, but instead of creating a new binding, it alters the value of an existing variable. You can use this to give new values to dynamic variables. For example:

# implement your own (move)
(gl/with [p (- p [0 (sin t * 50) 0])] (ball 100))

You can also use Bauble’s underscore notation to fit this into a pipeline:

(ball 100 | gl/with [p (- p [0 (sin t * 50) 0])] _)

You can – if you really want – use this to alter P or Q to not refer to the point in global space, or use it to pretend that the camera ray is actually coming at a different angle.

The variables you change in gl/with will, by default, apply to all of the fields of a shape. You can pass a keyword as the first argument to only change a particular field. This allows you to refer to variables that only exist in color expressions:

(gl/with :color [normal (normal + (perlin p * 0.1))]
  (ball 100 | shade [1 0 0]))

Uncategorized

(* & xs) source

Overloaded to work with tuples, arrays, and expressions.

(+ & xs) source

Overloaded to work with tuples, arrays, and expressions.

+x source

[1 0 0]

+y source

[0 1 0]

+z source

[0 0 1]

(- & xs) source

Overloaded to work with tuples, arrays, and expressions.

-x source

[-1 0 0]

-y source

[0 -1 0]

-z source

[0 0 -1]

(. expr field) source

Behaves like . in GLSL, for accessing components of a vector or struct, e.g. (. foo xy).

Bauble’s dot syntax, foo.xy, expands to call this macro. The second argument to . will be quasiquoted, so you can dynamically select a dynamic field with (. foo ,axis).

(/ & xs) source

Overloaded to work with tuples, arrays, and expressions.

(@and & forms) source

Evaluates to the last argument if all preceding elements are truthy, otherwise evaluates to the first falsey argument.

(@in ds key &opt dflt)

Get value in ds at key, works on associative data structures. Arrays, tuples, tables, structs, strings, symbols, and buffers are all associative and can be used. Arrays, tuples, strings, buffers, and symbols must use integer keys that are in bounds or an error is raised. Structs and tables can take any value as a key except nil and will return nil or dflt if not found.

(@length ds)

Returns the length or count of a data structure in constant time as an integer. For structs and tables, returns the number of key-value pairs in the data structure.

(@or & forms) source

Evaluates to the last argument if all preceding elements are falsey, otherwise evaluates to the first truthy element.

(atan2 y x) source

Returns a value in the range [-pi, pi] representing the angle between the (2D) +x axis and the point [x y].

This is an alternative to the built-in atan’s two argument overload that is defined when x = 0. You can also invoke this with a single vec2 whose coordinates will act as x and y.

See atan2+ for an angle in the range [0, tau).

(atan2+ y x) source

Like atan2, but returns a value in the range [0, tau) instead of [-pi, pi].

(cos+ x) source

Like cos, but returns a number in the range 0 to 1.

(cross-matrix vec) source

Returns the matrix such that (* (cross-matrix vec1) vec2) = (cross vec1 vec2).

(in & args) source

inf source

The number representing positive infinity

(nearest-distance) source

This is the forward declaration of the function that will become the eventual distance field for the shape we’re rendering. This is used in the main raymarcher, as well as the shadow calculations. You can refer to this function to sample the current distance field at the current value of p or q, for example to create a custom ambient occlusion value.

(osc &opt period lo hi) source

Returns a number that oscillates with the given period. There are several overloads:

# 0 to 1 to 0 every second
(osc t)

# 0 to 1 to 0 every 10 seconds
(osc t 10)

# 0 to 100 to 0 every 10 seconds
(osc t 10 100)

# 50 to 100 to 50 every 10 seconds
(osc t 10 50 100)

(oss &opt period lo hi) source

Like osc, but uses a sine wave instead of a cosine wave, so the output begins halfway between lo and hi.

pi source

I think it’s around three.

Note that there are also values like pi/4 and pi/6*5 and related helpers all the way up to pi/12. They don’t show up in autocomplete because they’re annoying, but they’re there.

(product v) source

Multiply the components of a vector.

(quantize value count) source

Rounds a value to the nearest multiple of count.

(remap+ x) source

Linearly transform a number in the range [-1 1] to [0 1].

(remap- x) source

Linearly transform a number in the range [0 1] to [-1 1]. The inverse of remap+.

(sin+ x) source

Like sin, but returns a number in the range 0 to 1.

(ss x [from-start] [from-end] [to-start] [to-end]) source

This is a wrapper around smoothstep with a different argument order, which also allows the input edges to occur in descending order. It smoothly interpolates from some input range into some output range.

(box [100 (ss p.z 100 -100 0 100) 100])

There are several overloads. You can pass one argument:

# (ss x) = (smoothstep 0 1 x)
(union
  (rect 50 | move y (sin t * 100) x -100)
  (rect 50 | move y (ss (sin t) * 100) x 100))

Three arguments (which is basically just smoothstep, except that you can reverse the edge order):

# (ss x from-start from-end) =
#   (if (< from-start from-end)
#     (smoothstep from-start from-end x)
#     (1 - smoothstep from-end from-start x))
(union
  (rect 50 | move y (sin t * 100) x -100)
  (rect 50 | move y (ss (sin t) 1 0.5 * 100) x 100))

Or five arguments:

# (ss x from [to-start to-end]) =
#   (ss x from * (- to-end to-start) + to-start)
(union
  (rect 50 | move y (sin t * 100) x -100)
  (rect 50 | move y (ss (sin t) 0.9 1 -100 100) x 100))

(sum v) source

Add the components of a vector.

tau source

Bigger than six, but smaller than seven.

Note that there are also values like tau/4 and tau/6*5 and related helpers all the way up to tau/12. They don’t show up in autocomplete because they’re annoying, but they’re there.

x source

[1 0 0]

y source

[0 1 0]

z source

[0 0 1]

Language

Bauble is implemented in the Janet programming language, and the programs you write in Bauble are just Janet programs that get evaluated with a particular environment already in scope.

If you’d like to learn more about Janet, I wrote a free book called Janet for Mortals that gives an introduction to the language.

Notation

Before Bauble executes your script, it performs three purely syntactic transformations on it. You can think of these like macros that are implicitly wrapped around every top-level expression, because that’s exactly what they are.

Dot notation

Dot notation gives you an easy way to access fields of vectors and structs. Symbols with dots inside them, like p.x, will be rewritten to (. p x), where . is an ordinary Janet macro defined in the Bauble standard environment.

Dot notation only applies to Janet symbols, so if you want to access a field of an expression, you have to invoke it using traditional prefix notation:

# this doesn't work!
(camera/perpsective [10 1 0] [0 0 0]).direction

# you have to do this instead:
(. (camera/perpsective [10 1 0] [0 0 0]) direction)

Just like in GLSL, dot notation supports vector swizzling:

(def radius [30 0])
(rect 100 :r radius.xyyx)

Pipe notation

Pipe notation is a way to write function applications in a postfix order, sort of like method-chaining in other languages.

# these two lines are the same
(move (circle 50) [100 0])
(circle 50 | move [100 0])

This is purely syntactic, like the -> or ->> macros in the Janet standard library.

By default the argument on the left of the pipe is inserted just after the first argument. But you can specify a different location for it using _:

# these two lines are the same
(move (circle 50) [100 0])
([100 0] | move (circle 50) _)

You can insert the left-hand side at any position you want:

# these two lines are the same
(move (circle 50) [100 0])
(move | _ (circle 50) [100 0])

I don’t know why you’d want to do that, but it’s nice to know you have the option.

Notice that if there are multiple expressions to the left of the pipe, as in circle 50 | move ..., they are implicitly wrapped in parentheses. But if there is only a single expression, it is not wrapped. So if you have a function that takes no arguments, you need to explicitly call it to use it with pipe notation: ((get-my-cool-shape) | move [50 0]).

Janet normally uses the pipe character as a way to create single-expression anonymous functions:

# this won't work in bauble
(map |(* 2 $) [1 2 3]) # [2 4 6]

But Bauble co-opts the character for postfix application, and doesn’t have anything to replace it with. So you just can’t. You have to make a normal fn:

(map (fn [$] (* 2 $)) [1 2 3]) # [2 4 6]

It’s not too bad. It’s a good tradeoff.

Finally, you can use pipe notation inside vectors:

[1 1 1 | normalize]

And save yourself some parens. This is the same as writing:

([1 1 1] | normalize)

Or:

(normalize [1 1 1])

Infix notation

In addition to |, four other symbols are treated specially: +, -, *, and /. These are interpreted as infix symbols, and they get rewritten like so:

# these two lines are the same
(circle (+ 10 20))
(circle (10 + 20))

There is no order of operations or precedence in Bauble’s infix notation. Operations always happen from left to right:

# these two lines are the same
(circle (* (+ 5 5) 10))
(circle (5 + 5 * 10))

You can still use prefix notation in Bauble; (+ 10 20) will not be rewritten to anything. + - * / are only special when they appear in the middle of a form like that. (Sometimes it’s nice to use the variadic prefix forms.)

If there are multiple forms to the left of an infix operator, they will be implicitly wrapped in parentheses, just like pipe notation:

# these two lines are the same
(circle (+ (* (sin+ t) 50) 50))
(circle (sin+ t * 50 + 50))

As an annoying corollary to this, there’s no way to pass any of these functions around as regular first-class functions. For example, this code would work in regular Janet:

(reduce + 0 [1 2 3])

But in Bauble, that gets rewritten to:

(+ reduce (0 [1 2 3]))

And you’ll get an inscrutable error. To work around this, Bauble defines the symbols @+, @-, @*, and @/ as aliases for the operators. These symbols have the same value, but the infix notation won’t treat them specially, so you can safely write this:

(reduce @+ 0 [1 2 3])

Although Bauble’s infix notation has no concept of precedence, the infix notation pass runs after the pipe notation pass. This means that you can use _ to move an expression “over” an infix operator. In practice this is useful to say e.g. (cos t | 1 - _).

You can also use infix notation inside a vector literal:

[1 0 1 * 100]

Which is the same as writing:

([1 0 1] * 100)

But with fewer parens.

If you’re ever confused by what Bauble’s notation is doing, you can see what your code expands to like this:

(pp '(p.x | 1 + 2 * _))

Bauble CLI

Okay so there is, technically, a command-line version of Bauble. You can run it locally using native OpenGL instead of WebGL, and it has exciting features like exporing an OBJ file and rendering non-square images.

But it’s like pre-pre-alpha quality, and the only way to get it is to build it from source, and I’m sorry about that. There are instructions in the GitHub repo.