@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
| Shape | Sizes | Default |
|---|---|---|
sphere | r | r: .3 |
ellipsoid | r: x y z | r: .3 |
box | size, one value or x y z; round | size: .5 |
cylinder | r, height along y; round | r: .2; height: .5 |
cone | r: bottom top, height along y | r: .2 0; height: .4 |
torus | r: ring tube, the ring around y | r: .3 .08 |
capsule | from: x y z, to: x y z, r, or r: start end | r: .1, both ends at 0 0 0 |
polygon | a @shape body, points: x y, …, text or path in the xy plane; depth along z, round | depth: .1 |
lathe | a @shape body, points: x y, …, text or path, turned around y | |
plane | none; everything below y = 0, to cut with in a group | |
sdf | d, a distance written in p |
Rows
| Row | Meaning |
|---|---|
move | offset x y z, with y pointing up; one value moves along all three axes |
rotate | counterclockwise 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. |
scale | one factor, or one per axis; a negative one mirrors |
grow | makes the shape bigger by this much |
frame | keeps a shell this thick inside the surface |
round | rounds the edges of a box, cylinder or polygon, keeping its outer size |
twist | degrees of turn for each unit up y |
mirror | the axes to reflect across, x, y, z |
repeat | spacing x y z of endless copies, 0 for none; one value repeats along all three axes |
fill | a color, a list of channels such as .2, .4, .6, or an expression in p, normal and light that gives one; alpha is ignored |
smooth | on a group or on copies such as sphere*5: the radius of the curve where its shapes meet |
view | at the top: turn tilt of the camera, in degrees |
perspective | at the top: the camera distance, 2 by default, or none |
zoom | at the top: scales the fitted view about its center, 1 by default |
drag | at the top: turn, tilt or both, what the pointer may do to the view; none by default |
light | at 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. |
shadow | at the top: 0 to 1, how much blocked light and crowded corners darken; none by default |
ambient | at the top: sky ground, the light from all around, .3 by default. One value is dimmer facing down, and 0 leaves only the lights. |
| name | any 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
| Name | Value |
|---|---|
p | the point in the shape's own space, a read-only vec3 |
t | time in seconds; reading it animates the scene |
n, N, nx, ny | the copy's number from 1, the number of copies, and the copy's column and row; all 1 outside copies |
normal | in fill only: the surface normal as the camera sees it |
light | in 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, turn | a 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.