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 ctrlspace
to trigger it manually.
Reference

Shapes
 3D shapes
 2D shapes
 Shape combinators
 Shape functions
 Lowerlevel shape stuff
 Transformations
 Dynamic variables
 Bauble variables

Shading
*lights*
blinnphong
bump
calculategradient
calculatenormal
castlighthardshadow
castlightnoshadow
castlightsoftshadow
color
fresnel
graydient
isolines
Light
light/ambient
light/directional
light/map
light/mapbrightness
light/mapcolor
light/point
light?
normal+
occlusion
shade
tint
unioncolor
withlights
 Repetition
 Noise
 Camera
 Colors
 Rotation
 GLSL helpers
 Uncategorized
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)])
(boxframe size thickness [:r round])
source
Returns a 3D shape, the outline of a box.
(union
(boxframe 100 5 :r (osc t 3 5))
(boxframe [(osc t 4 30 100) (osc t 5 30 100) (oss t 6 30 100)] 1))
(capsule axis length radius [topradius])
source
There are two types of capsule
s: symmetric capsules, which look
like pills, or axisaligned 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 fromradius [toradius])
source
Returns a line between two points.
(line
[100 (sin t * 100) (cos t * 100)]
[100 (cos t * 100) (sin t * 100)]
10
 union (boxframe 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 (boxframe 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))
(capsule2d bottomradius topradius height)
source
(capsule2d 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 :samplefrom 1 :sampleto 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.
(cutdisk radius bottom)
source
Returns a 2D shape.
(cutdisk 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))
(orientedrect 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))
(quadcircle radius)
source
Returns a 2D shape, an approximation of a circle made out of quadratic bezier curves.
(quadcircle 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 [topleft
topright
bottomright
bottomleft]
.
(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 outerradius innerradius [:r round])
source
(star 100 70 :r (osc t 3 20))
(trapezoid bottomwidth topwidth 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 (boxframe 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 darkgray))
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 differentlyshaped 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 twodimensional slice of a threedimensional 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 perfield
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
(mapcolor 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/mapcolor
in that the expression is wrapped in gl/let
,
so you can refer to it multiple times.
(mapdistance 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/mapdistance
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.
Lowerlevel shape stuff
(shape/color shape)
source
Shorthand for (shape/getfield shape :color)
.
(shape/distance shape)
source
Shorthand for (shape/getfield shape :distance)
.
(shape/getfield 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/mapfields
.
(shape/mapcolor shape f)
source
Shorthand for (shape/mapfield shape :color f)
.
(shape/mapdistance shape f)
source
Shorthand for (shape/mapfield shape :distance f)
.
(shape/mapfield shape field f)
source
Map a single field on a shape. If the field does not exist, this does nothing.
(shape/mapfields 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 destshape field sourceshape)
source
Shorthand for (shape/with destshape field (shape/getfield sourceshape 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 & newkvs)
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 boundingshape 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 finetune 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 vectorscalar 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 counterclockwise 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 builtin 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 rayorigin 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.
fragcoord
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 (FragCoord  (resolution * 0.5) / max resolution)
.
FragCoord
source
The center of the current pixel being rendered. Pixel centers are at [0.5 0.5]
,
so with no antialiasing this will have values like [0.5 0.5]
, [1.5 0.5]
, etc.
If you’re using multisampled antialiasing, this will have offcentered 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 global point in 2D space.
q
source
The local point in 2D space. This is the position being shaded, with any transformations applied.
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 dir
ection. 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 quadview or a chunked render,
it may have a different origin or resolution.
You can use (glfragcoord.xy  viewport.xy)
in order to get the logical fragment position (the value
exposed to a typical shader as FragCoord
).
Bauble variables
aagridsize
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 antialiasing).
backgroundcolor
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 backgroundcolor transparent)
camera
source
An expression for a ray
that determines the position and direction of the camera.
default2dcolor
source
A variable that determines the default color to use when rendering a 2D shape with no color field.
Default is isolines
.
default3dcolor
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 withdyns
like any other
dynamic variable, but there is a dedicated withlights
function to
set it in a way that fits nicely into a pipeline.
(blinnphong light color [:s shininess] [:g glossiness])
source
A BlinnPhong 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 blinnphong [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 actuallybumpy 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 calculatenormal
,
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)))
(calculategradient expr)
source
Evaluates the given 2D distance expression four times, and returns an approximation of the expression’s gradient.
(calculatenormal expr)
source
Evaluates the given 3D distance expression four times, and returns an approximation of the expression’s gradient.
(castlighthardshadow lightcolor lightposition)
source
TODOC
(castlightnoshadow lightcolor lightposition)
source
TODOC
(castlightsoftshadow lightcolor lightposition 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/mapbrightness light f)
source
f
takes and returns a :float
expression.
(light/mapcolor 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 stepcount] [: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 10
). 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 mapreduce over the current lights.
It’s a higherorder operation that takes a shading function (by
default blinnphong
) – 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 blinnphong
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 specialcase the zero vector (ambient
lights), and theoretically if you want to do anything positiondependent 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))
(unioncolor & shapes [:r r] [:s s])
source
unioncolor
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  unioncolor (circle 60  color orange))
(ball 100
 shade sky
# change this to a union
 unioncolor (star 50 30  color red  extrude z inf  radial y 5  rotate y t))
(withlights 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]
 withlights
(light/point 0.5 [100 100 0])
(light/ambient 0.5))
Repetition
(radial shape [axis] count [offset] [:oversample oversample] [:samplefrom samplefrom] [:sampleto sampleto])
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 :samplefrom 0
:sampleto 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 :samplefrom 1
, you can also look at the “far” slice.
By passing :samplefrom 0 :sampleto 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 :samplefrom 1)
(radial* [axis] count [offset] getshape [:oversample oversample] [:samplefrom samplefrom] [:sampleto sampleto])
source
Like radial
, but the shape is a result of invoking getshape
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] [:samplefrom samplefrom] [:sampleto sampleto])
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 :samplefrom 0
:sampleto 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 :samplefrom 1
, you can also look at the tile to the left.
By passing :samplefrom 0 :sampleto [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;
:samplefrom 1
:sampleto 1
will do 27.
(tile* size getshape [:limit limit] [:oversample oversample] [:samplefrom samplefrom] [:sampleto sampleto])
source
Like tile
, but the shape is a result of invoking getshape
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 highfrequency 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 highfrequency 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 highfrequency 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 highfrequency 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))
(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 secondnearest 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 perspectiveadjusted ray from this camera for
the current fragcoord
. 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
(perspectivevector fov)
source
Returns a unit vector pointing in the +z
direction for the
given camera fieldofview (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 backgroundcolor black)
(ball 100  shade black)
blue
source
(set backgroundcolor blue)
(ball 100  shade blue)
cyan
source
(set backgroundcolor cyan)
(ball 100  shade cyan)
darkgray
source
(set backgroundcolor darkgray)
(ball 100  shade darkgray)
gray
source
(set backgroundcolor gray)
(ball 100  shade gray)
green
source
(set backgroundcolor green)
(ball 100  shade green)
hotpink
source
(set backgroundcolor hotpink)
(ball 100  shade hotpink)
(hsl hue saturation lightness)
source
Returns a color.
(hsv hue saturation value)
source
Returns a color.
lightgray
source
(set backgroundcolor lightgray)
(ball 100  shade lightgray)
lime
source
(set backgroundcolor lime)
(ball 100  shade lime)
magenta
source
(set backgroundcolor 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 highchroma 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/ofrgb rgb)
source
Convert an Oklab color to a linear RGB color. You can use this,
along with ok/torgb
, 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  mapcolor ok/ofrgb)
(box 50  shade blue  mapcolor ok/ofrgb)
 mapcolor ok/torgb
 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  mapcolor ok/ofrgb)
(ball 40  shade blue  mapcolor ok/ofrgb  move y 50)
 mapcolor ok/torgb
 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/torgb ok)
source
Convert a linear RGB color to the Oklab color space. See ok/ofrgb
for examples.
orange
source
(set backgroundcolor orange)
(ball 100  shade orange)
purple
source
(set backgroundcolor purple)
(ball 100  shade purple)
red
source
(set backgroundcolor red)
(ball 100  shade red)
sky
source
(set backgroundcolor sky)
(ball 100  shade sky)
teal
source
(set backgroundcolor 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 backgroundcolor transparent)
white
source
(set backgroundcolor white)
(ball 100  shade white)
yellow
source
(set backgroundcolor yellow)
(ball 100  shade yellow)
Rotation
(alignmentmatrix 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.
(rotationaround axis angle)
source
A rotation matrix about an arbitrary axis. More expensive to compute than the axisaligned rotation matrices.
(rotationmatrix & args)
source
Return a rotation matrix. Takes the same arguments as rotate
, minus the initial thing to rotate.
(rotationx angle)
source
A rotation matrix about the X axis.
(rotationy angle)
source
A rotation matrix about the Y axis.
(rotationz angle)
source
A rotation matrix about the Z axis.
GLSL helpers
(gl/def name expression)
source
You can use gl/def
to create new toplevel GLSL variables which will only
be evaluated once (per distance and color field evaluation). This is useful in
order to reuse 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 toplevel, 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 returntype 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 "optionallabel"
(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 "optionallabel"
(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 immediatelyinvoked 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 keyvalue 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 builtin 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
.
(cos x)
source
Like cos
, but returns a number in the range 0
to 1
.
(crossmatrix vec)
source
Returns the matrix such that (* (crossmatrix vec1) vec2)
= (cross vec1 vec2)
.
(in & args)
source
inf
source
The number representing positive infinity
(nearestdistance)
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 [1 1]
to [0 1]
.
(sin+ x)
source
Like sin
, but returns a number in the range 0
to 1
.
(sin x)
source
Like sin
, but returns a number in the range 0
to 1
.
(ss x [fromstart] [fromend] [tostart] [toend])
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 fromstart fromend) =
# (if (< fromstart fromend)
# (smoothstep fromstart fromend x)
# (1  smoothstep fromend fromstart 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 [tostart toend]) =
# (ss x from * ( toend tostart) + tostart)
(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 toplevel 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 methodchaining 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 lefthand 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: ((getmycoolshape)  move [50 0])
.
Janet normally uses the pipe character as a way to create singleexpression anonymous functions:
# this won't work in bauble
(map (* 2 $) [1 2 3]) # [2 4 6]
But Bauble coopts 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 firstclass 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 commandline 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 nonsquare images.
But it’s like preprealpha 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.