On this page:
5.1 Scene Combiners
Tag
tag?
group
group-tag
group-contents
basis
ungroup
remove-group
remove-in-group
replace-group
replace-in-group
set-origin
join
glue
pin
weld
pin*
weld*
join*
glue*
map-group
map-group/  transform
find-group-transform
find-group-transforms

5 Combining Scenes🔗ℹ

In 3D space, the terms horizontal and vertical are ambiguous.

When there’s no unique or preferred way to view an object, even the terms top, bottom, left, right, front and back are ambiguous.

When an object is squashed or rotated, or is irregularly shaped, these terms are ambiguous even with a preferred orientation. For example, if we say that the following ellipsoid is leaning to the right, there are at least two acceptable ways to interpret the term on the left side:
> (define p1 (rotate-y (ellipsoid origin (dir 1/4 1/4 1)) -30))
> (define camera (basis 'camera (point-at (pos 1 1 4) (dir -1 -1 0))))
> (combine (move-z p1 4)
           (with-emitted (emitted "green")
             (arrow (pos 0.75 0 4.25) (dir -0.45 0 -0.25)))
           (with-emitted (emitted "red")
             (arrow (pos 1.05 0 3.25) (dir -0.5 0 0)))
           camera)

image

Now suppose we decide that the green arrow points at the “left side,” and that we want to attach the face of a cube to it. There are infinitely many ways to do so: one for each point on the face and each angle of rotation.

In general, combining two 3D scenes relationally requires specifying a lot of data.

Pict3D tries to reduce the burden by
  • Allowing Pict3D instances to be combined by naming groups in each instance to attach together.

  • Making it easy to add such groups at specific points with specific orientations.

For example, the following code defines a basis, which is an empty group, on the left side of the ellipsoid p1, pointing away from the surface.
> (define-values (v dv) (surface/normal p1 +x))
> (define left (basis 'left (point-at v dv)))
Here, the surface/normal function finds a point on the surface of p1 in the direction +x. It returns that point and a surface normal, which is the direction perpendicular to the surface at that point. The point-at function returns an affine transformation that represents pointing in the direction dv from position v. Finally, basis creates a Pict3D out of the result.

We could display left, but it’s more instructive to combine it with the ellipsoid whose surface it’s on.
> (define p1/left (move-z (combine p1 left) 4))
> (combine p1/left camera)

image

(We’ve moved it upward 4 units only to keep the origin’s axes from obscuring the new basis.) Notice that the blue arrow points outward.

Now we’ll create a cube to attach onto that basis.
> (define p2 (with-color (rgba "deepskyblue" 0.75)
               (cube origin 1/4)))
> (define top (basis 'top (point-at (surface p2 +z) -z)))
> (define p2/top (move-z (combine p2 top) 4))
> p2/top

image

(Again, we’ve moved it upward, away from the origin’s axes.) This time, we used surface instead of surface/normal because we know the surface normal: +z. But we haven’t applied point-at to +z, we’ve used -z instead, causing the blue arrow to point in the direction opposite the surface normal: downward, into the cube.

All we have left is to pin the scenes together:
> (define p3 (pin p1/left '(left)
                  p2/top '(top)))
> (combine p3 camera)

image

Because (pin pict1 path1 pict2 path2) puts pict2 inside of the group path1, it naturally creates group hierarchies. When this isn’t what you want, use join instead. This has applied the transformation necessary to make the basis named 'top in p2/top match the basis named 'left in p1/left, and combined the transformed p2/top with p1/left. In other words, it’s rotated and moved p2/top so that top’s red arrow is the same as left’s red arrow, top’s green arrow is the same as left’s green arrow, and top’s blue arrow is the same as left’s blue arrow.

(We refer to the group named 'left using the tag path '(left).)

Suppose we want to rotate the cube 30 degrees. We have a few options.
  • Recreate p1/left with a rotated left basis, and then recreate p3.

  • Recreate p2/top with a rotated top basis, and then recreate p3.

  • Replace the group named 'left in p3 with the same group rotated 30 degrees.

The first option only requires us to replace (basis 'left (point-at v dv)) in the definition of left with (basis 'left (point-at v dv #:angle 30)). The second option requires similar changes to top. But at this point, with p3 already defined, both are more work than the third option, which is simply this:
> (combine (replace-group p3 '(left) (λ (p) (rotate-z p 30)))
           camera)

image

This replaces, in p3, every group p named 'left with (rotate-z p 30). The cube rotates because the group named 'left in p3 isn’t empty any longer: it’s been filled with a transformed p2/top.

It’s instructive to consider what would have happened if we’d had p2/top’s basis point in the direction of the surface normal; i.e. if we had used +z instead of -z.
> (define top (basis 'top (point-at (surface p2 +z) +z)))
> (define p2/top (move-z (combine p2 top) 4))
> p2/top

image

> (combine (pin p1/left '(left)
                p2/top '(top))
           camera)

image

Generally, when we want to pin two Pict3D instances together using bases, one’s basis should point inward, and the other’s should point outward.

5.1 Scene Combiners🔗ℹ

type

Tag

predicate

tag? : (-> Any Boolean : Tag)

The type and predicate for names of groups. Currently, Tag is defined as (U Symbol Integer).

Functions that operate on groups do not accept a Tag value on its own. They accept a tag path, which is a list of Tag values. This allows them to operate on groups within groups. For example, to remove the contents of the group named 'cannon within a group named 'player, we might write

(remove-in-group world '(player cannon))

Then, only the poor player will be left without a printer.

To remove the contents of every cannon group regardless of the group that contains it, we might write

(remove-in-group world '(cannon))

The tag path '() or empty refers to an entire Pict3D. Thus,

(remove-in-group world '())

returns an empty Pict3D.

procedure

(group pict name)  Pict3D

  pict : Pict3D
  name : (U #f Tag)

procedure

(group-tag pict)  (U #f Tag)

  pict : Pict3D

procedure

(group-contents pict)  Pict3D

  pict : Pict3D
(group pict name) creates a group: a named collection of shapes. If name is #f, it simply returns pict, whose tag path is '(). Otherwise, the new group can be referred to by its tag path (list name).

If pict is a group, group-tag returns its name and group-contents returns its contents. If pict isn’t a group, group-tag returns #f and group-contents returns pict.

These seemingly strange rules preserve the following properties.

procedure

(basis name t)  Pict3D

  name : Tag
  t : Affine
Creates an empty group named name with transformation t. Equivalent to (transform (group empty-pict3d name) t).

procedure

(ungroup pict path)  Pict3D

  pict : Pict3D
  path : (Listof Tag)
Removes every group with the given path, but leaves the contents. Equivalent to (replace-group pict path group-contents).

procedure

(remove-group pict path)  Pict3D

  pict : Pict3D
  path : (Listof Tag)
Removes every group with the given path, erasing the contents. Equivalent to (replace-group pict path (λ (_) empty-pict3d)).

procedure

(remove-in-group pict path)  Pict3D

  pict : Pict3D
  path : (Listof Tag)
Removes the contents of every group with the given path, but leaves the group. Equivalent to (replace-in-group pict path (λ (_) empty-pict3d)).

procedure

(replace-group pict path f)  Pict3D

  pict : Pict3D
  path : (Listof Tag)
  f : (-> Pict3D Pict3D)
Replaces every group p in pict with the given path with (f p).

procedure

(replace-in-group pict path f)  Pict3D

  pict : Pict3D
  path : (Listof Tag)
  f : (-> Pict3D Pict3D)
Replaces the contents p of every group in pict with the given path with (f p). Equivalent to (replace-group pict path (λ (p) (group (f (group-contents p)) (group-tag p)))).

procedure

(set-origin pict path)  Pict3D

  pict : Pict3D
  path : (U (Listof Tag) Affine)
Transforms pict so that the group with the given tag path, or the given affine transformation, is aligned with the origin.

Examples:
> (define pict
    (combine (cube origin 1/2)
             (basis 'corner (point-at (pos 1/2 1/2 1/2) +x+y+z
                                      #:angle 15))))
> pict

image

> (set-origin pict '(corner))

image

If there is more than one group with the given path, set-origin raises an error.

procedure

(join pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (U (Listof Tag) Affine)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()
Does two things:
  1. Transforms pict2 so that its group with path path2 aligns with the group in pict1 with path path1.

  2. Combines the result with pict1.

If path1 or path2 is given as an affine transformation, join uses them directly instead of looking up a group transformation.

If there is more than one group with path1 in pict1, join raises an error, and likewise for path2 in pict2.

Recall that the tag path '() represents the entire Pict3D. Thus, when path2 is '() or is omitted, the origin and coordinate axes are used for alignment.

Examples:
> (define p1 (combine (cylinder origin 1/2)
                      (basis 'top (point-at (pos 0 0 1/2) +z))))
> p1

image

> (define p2 (cone origin 1/2))
> p2

image

> (join p1 '(top) (move-z p2 1/2))

image

> (join p1 '(top)
        (combine p2 (basis 'bot (point-at (pos 0 0 -1/2) +z)))
        '(bot))

image

When path1 and path2 are tag paths, (join pict1 path1 pict2 path2) is equivalent to
(combine
 pict1
 (relocate pict2
           (find-group-transform pict2 path2)
           (find-group-transform pict1 path1)))
When path1 and path2 are affine transformations, (join pict1 path1 pict2 path2) is equivalent to

(combine pict1 (relocate pict2 path2 path1))

procedure

(glue pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (U (Listof Tag) Affine)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()
Like (join pict1 path1 pict2 path2), but ungroups all groups in pict1 that have path path1, and ungroups all groups in pict2 that have path path2.

Like join, glue accepts affine transformations as well as tag paths.

procedure

(pin pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (Listof Tag)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()
Similar to join, but additionally
  • Ungroups the group path2 in pict2.

  • Puts pict2 inside the group path1 in pict1.

Use pin to construct group hierarchies, in which updates to parent groups (using replace-group or replace-in-group) affect child groups. See Combining Scenes for an extended example.

In code, (pin pict1 path1 pict2 path2) is equivalent to
(let ([pict2  (ungroup (set-origin pict2 path2) path2)])
  (replace-in-group pict1 path1 (λ (p) (combine p pict2))))

Like join, pin accepts an affine transformation for its second argument.

procedure

(weld pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (Listof Tag)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()
Like (pin pict1 path1 pict2 path2), but additionally ungroups all groups in pict1 that have path path1.

Use weld instead of pin when you don’t intend to update the group with path path1 in the result. For example, use pin to attach a swinging arm to a robot body, and use weld to place a roof on top of a house. (Unless you intend to blow up the house later. Then you’ll need to pin the roof.)

Like glue, weld accepts an affine transformation for its second argument.

procedure

(pin* pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (Listof Tag)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()

procedure

(weld* pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (Listof Tag)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()

procedure

(join* pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (U (Listof Tag) Affine)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()

procedure

(glue* pict1 path1 pict2 [path2])  Pict3D

  pict1 : Pict3D
  path1 : (U (Listof Tag) Affine)
  pict2 : Pict3D
  path2 : (U (Listof Tag) Affine) = '()
Like pin, weld, join, and glue, but if multiple groups with path path1 exist in pict1, pict2 is pinned, welded, joined, or glued to all of them.

procedure

(map-group pict path f)  (Listof A)

  pict : Pict3D
  path : (Listof Tag)
  f : (-> Pict3D A)
Applies f to every group with the given path in pict, and returns the results in a list. The list is in no particular order.

procedure

(map-group/transform pict path f)  (Listof A)

  pict : Pict3D
  path : (Listof Tag)
  f : (-> Affine Pict3D A)
Like (map-group pict name f), but f is additionally supplied the transformation from world coordinates to group coordinates.

procedure

(find-group-transform pict path)  Affine

  pict : Pict3D
  path : (Listof Tag)
Finds the group within pict with path, and returns the transformation from world coordinates to that group’s coordinates.

If multiple groups with path exist, find-group-transform raises an error.

This is used in the implementations of functions like join and set-origin.

procedure

(find-group-transforms pict path)  (Listof Affine)

  pict : Pict3D
  path : (Listof Tag)
Like find-group-transform, but if multiple groups with path exist, returns a transformation for each of them.

Equivalent to:

(map-group/transform pict path (λ ([t : Affine] _) t))