Function

@scene

A 3D model built from shapes, raymarched on the GPU with WebGL into an image. It is written in the same language as @shape, with shapes in three dimensions.

Syntax

  • @scene(<body>)
  • @scene<width>x<height>(<body>)

The body is a list of shapes, each written name { … }, and rows such as view: 35 25. Digits after the name set the image size in pixels (@scene800 is square); without them it takes the size of the element. The model has no background of its own.

Put a moving or dragged scene in @content to draw a live <canvas>. As a background, every frame is encoded into a new image, which is much slower.

@grid: 1 / 60vmin;
@content: @scene(
  box { size: 1; round: .1; fill: #f06 }
  view: 35 25;
);

Shapes

Each shape takes its own sizes, listed in the table below. The camera pulls back to fit the model but never comes closer, so sphere { r: .5 } about fills a square image. fill gives each shape a color:

@grid: 1 / 60vmin;
@content: @scene(
  cylinder { r: .3; height: .8; fill: #fc0 }
  cone { r: .4 0; height: .6; move: 0 .7 0; fill: #f06 }
  view: 0 20;
);

Moving shapes

move: x y z shifts a shape, with y pointing up. rotate turns it counterclockwise, in degrees, around z or an axis written before the angle, as in rotate: x 90. scale takes one factor, or one per axis. They run in the order written.

@ functions in the body run for each cell, and $name inserts the cell's --name, so every cell can draw its own model:

@grid: 4 / 60vmin / #111;
@content: @scene(
  box {
    size: .6;
    round: .06;
    rotate: 1 1 1 @r(360);
    fill: @p(#f06, #fc0, #0cf, #fff);
  }
);

Combining shapes

union, subtract and intersect join the shapes inside them. subtract takes the rest away from the first one. A rounded cube with three holes through it:

@grid: 1 / 60vmin;
@content: @scene(
  subtract {
    intersect {
      box { size: 1 }
      sphere { r: .68 }
    }
    cylinder { r: .3; height: 2 }
    cylinder { r: .3; height: 2; rotate: x 90 }
    cylinder { r: .3; height: 2; rotate: z 90 }
  }
  view: 35 25;
);

smooth rounds the joins with a curve of that radius, and blends the colors where the shapes meet. The shapes in a group take its fill unless they have their own:

@grid: 1 / 60vmin;
@content: @scene(
  union {
    smooth: .15;
    fill: #fc0;
    sphere { r: .5 }
    sphere { r: .35; move: .55 .3 0; fill: #f06 }
  }
);

Names and copies

name#id keeps a shape for later instead of drawing it, and #id draws it, with any rows given there added. name*N repeats a shape, and name*XxY repeats it in a grid:

@grid: 1 / 60vmin;
@content: @scene(
  box#leg { size: .06 .5 .06 }
  box { size: 1 .06 .7; move: 0 .28 0; round: .02 }
  #leg*2x2 { move: nx * .84 - 1.26, 0, ny * .54 - .81 }
  view: 35 25;
);

Each copy reads its number as n, from 1, and the count as N:

@grid: 1 / 60vmin;
@content: @scene(
  sphere*12 {
    r: .12;
    move: .8 0 0;
    rotate: y n * 30;
    fill: hsl(n / N, .7, .6);
  }
  view: 0 30;
);

From an outline

polygon takes a @shape body as its outline, holes and all, and gives it a depth. The outline is in the units of @shape, where the box is 2 across. points: x y, … lists the corners instead:

@grid: 1 / 60vmin;
@content: @scene(
  polygon { heart; depth: .3; fill: #e33 }
  view: 30 20;
);

Both polygon and lathe also take text with font, or path, as @shape does, so polygon { text: A; depth: .3 } is a solid letter (Letters and paths).

lathe turns the right half of an outline around y:

@grid: 1 / 60vmin;
@content: @scene(
  lathe { vase; fill: #4a8 }
  view: 0 20;
);

In a lathe, the x = 0 of a path is the axis and text starts at it, so lathe { text: 'abc' } turns every letter.

The outline reads the n of its own copies, so lathe*3 { circle { r: .1n } } gives three sizes. Past 32 copies, they all take the first one's outline.

Expressions

Values are expressions as in @pattern, with the names below instead of its cell names. fill may be an expression in p, the point in the shape's own space, that gives a color:

@grid: 1 / 60vmin;
@content: @scene(
  torus { r: .5 .2; fill: mix(#f06, #0cf, p.x + .5) }
  view: 20 50;
);

fill can also read normal, the surface normal as the camera sees it, with x to the right, y up and z toward the camera:

@grid: 1 / 60vmin;
@content: @scene(
  box { size: 1; round: .2; fill: .5 + .5 * normal }
  view: 35 25;
);

sdf { d: … } draws any shape from its signed distance, written in p:

@grid: 1 / 60vmin;
@content: @scene(
  sdf {
    d: length(p) - .6
      + .04 * sin(20 * p.x) * sin(20 * p.y) * sin(20 * p.z);
  }
);

The 2D distances of @pattern, such as arc and box, work on two axes of p. Here the distance to a three-quarter arc in the xy plane, taken with p.z, makes a bent tube:

@grid: 1 / 60vmin;
@content: @scene(
  sdf {
    d: length(vec2(arc(p.xy, .5, 0, 1.5π), p.z)) - .08;
    fill: #f06;
  }
  view: 20 30;
);

Bending space

twist: a turns a shape a degrees for each unit up y. mirror: x z keeps the shape and its reflections across those axes, and repeat: x y z repeats it that far apart along each axis, 0 for none:

@grid: 1 / 60vmin;
@content: @scene(
  box { size: .5 1.5 .5; twist: 90; fill: #0cf }
  view: 30 15;
);

Camera

view: turn tilt turns the camera around y and tilts it down, in degrees. perspective: d sets its distance, and none gives a parallel view:

@grid: 1 / 60vmin;
@content: @scene(
  box*5x5 {
    size: .3;
    round: .03;
    move: nx * .4 - 1.2, 0, ny * .4 - 1.2;
    fill: hsl(n / N, .6, .6);
  }
  view: 45 35;
  perspective: none;
);

zoom: k then scales the view about its center, and above 1 it crops the model.

drag: turn tilt lets the pointer turn and tilt the view, half a turn across the drawing's shorter side; drag: turn only turns it. The camera keeps the distance it has without drag, so the perspective stays the same, and widens its view only where the model would not fit from every side:

@grid: 1 / 60vmin;
@content: @scene(
  box { size: .7; round: .08; fill: #fc0 }
  torus { r: .55 .06; fill: #f06 }
  view: 30 25;
  drag: turn tilt;
);

Light

One light shines from the upper right. light: x y z moves it to that direction, in the axes of normal, so it turns with the camera. shadow, from 0 to 1, lets shapes block it:

@grid: 1 / 60vmin;
@content: @scene(
  sphere { r: .4; fill: #fc0 }
  box { size: 2 .1 2; move: 0 -.45 0; fill: #eee }
  view: 30 30;
  light: -1 1 .5;
  shadow: .8;
);

A color after the direction tints the light, and lights separated by commas add up. ambient: sky ground fills the shade, sky on surfaces facing up in the scene and ground on those facing down, so unlike light it stays put when the view tilts:

@grid: 1 / 60vmin;
@content: @scene(
  torus { r: .5 .2; fill: #fff }
  view: 20 50;
  light: -1 1 .5 #ffc98a, 1 0 .2 #5a7cff * .8;
  ambient: #3a4a7a #4a2a1a;
);

A fill that reads light, how much light reaches the point, does its own lighting, and the scene leaves it as it is. Rounding light down paints in bands, shadows too:

@grid: 1 / 60vmin;
@content: @scene(
  sphere { r: .4; move: 0 .1 0 }
  box { size: 2 .1 2; move: 0 -.35 0 }
  fill: mix(#5e6472, #e9d8a6, floor(light * 4) / 4);
  view: 30 30;
  shadow: 1;
);

Motion

A value that reads t, the time in seconds, makes the scene move, and view: 60t spins it in place. The camera fits the model over its first 2π seconds, so a motion that repeats within that time leaves it still:

@grid: 1 / 60vmin;
@content: @scene(
  box { size: .8; round: .08; rotate: 1 1 0 45; fill: #fc0 }
  view: 60t 20;
);

Reference

Shapes

ShapeSizesDefault
sphererr: .3
ellipsoidr: x y zr: .3
boxsize, one value or x y z; roundsize: .5
cylinderr, height along y; roundr: .2; height: .5
coner: bottom top, height along yr: .2 0; height: .4
torusr: ring tube, the ring around yr: .3 .08
capsulefrom: x y z, to: x y z, r, or r: start endr: .1, both ends at 0 0 0
polygona @shape body, points: x y, …, text or path in the xy plane; depth along z, rounddepth: .1
lathea @shape body, points: x y, …, text or path, turned around y
planenone; everything below y = 0, to cut with in a group
sdfd, a distance written in p

Rows

RowMeaning
moveoffset x y z, with y pointing up; one value moves along all three axes
rotatecounterclockwise rotation in degrees around z, x, y, or an axis x y z before the angle, as in rotate: 1 1 0 45. 90deg and .25turn work too.
scaleone factor, or one per axis; a negative one mirrors
growmakes the shape bigger by this much
framekeeps a shell this thick inside the surface
roundrounds the edges of a box, cylinder or polygon, keeping its outer size
twistdegrees of turn for each unit up y
mirrorthe axes to reflect across, x, y, z
repeatspacing x y z of endless copies, 0 for none; one value repeats along all three axes
filla color, a list of channels such as .2, .4, .6, or an expression in p, normal and light that gives one; alpha is ignored
smoothon a group or on copies such as sphere*5: the radius of the curve where its shapes meet
viewat the top: turn tilt of the camera, in degrees
perspectiveat the top: the camera distance, 2 by default, or none
zoomat the top: scales the fitted view about its center, 1 by default
dragat the top: turn, tilt or both, what the pointer may do to the view; none by default
lightat the top: x y z toward a light, .6 .8 .5 by default, then a color, or a number for a white light's strength. Commas add lights, and none leaves the fill unshaded.
shadowat the top: 0 to 1, how much blocked light and crowded corners darken; none by default
ambientat the top: sky ground, the light from all around, .3 by default. One value is dimmer facing down, and 0 leaves only the lights.
nameany other name declares a variable for the rows and shapes of its block, such as k: .3 or c: #f06. Reading p, it sees the point after the block's own steps. One that nothing reads is reported, which is how a misspelled row shows.

move, rotate, scale, grow, frame, mirror, repeat and twist run in the order written; for every other row the last one wins. Rows at the top, other than the camera and light rows, apply to the whole model as they would to a group.

Names

NameValue
pthe point in the shape's own space, a read-only vec3
ttime in seconds; reading it animates the scene
n, N, nx, nythe copy's number from 1, the number of copies, and the copy's column and row; all 1 outside copies
normalin fill only: the surface normal as the camera sees it
lightin fill only: how much light reaches the point, shadows included, about 1 facing a white light, and 1 with light: none
rand()a random number that differs from copy to copy, though not from cell to cell
deg, turna degree and a turn in radians, so sin(90deg) is 1
πpi

The operators and functions are those of @pattern, so ** is a power and ^ a bitwise xor. There are no match { … } blocks; a value picks with match(test, a, b). $name(…) calls a function defined with --name(). @pn and @plot step from cell to cell, and from copy to copy inside a copy such as sphere*5. The pointer reaches a scene only through drag.

See also