gray kangaroo

A code-along is a story about coding in which you are encouraged to “code up” all the examples in the story. In a code-along you shouldnot cut-and-paste.

In this code-along we will explore how 2-dimensional LEGO artifacts can be projected onto 3-dimensional artifacts. For example, a chessboard pattern can be projected onto a sphere. One can also project 2-dimensional pixel art onto a sphere. We will explore two ways in which such projection can be accomplished.

Code-along Program 1

Program 1 uses a traversal to create a picture frame consisting of a black border and red center. Note that the variable onFrame holds the result of a Boolean expression that evaluates to true when the x and z values of a point have properties that are necessary (though notsufficient) for points that reside in the frame of the picture; otherwise the point has properties that are necessary for points that reside in the center portion of the picture frame. In order for a point to reside in the picture frame, it must also have a y value equal to 0. A nested conditional expression in the body of the function buildPictureFrame assures that points in the picture frame (1) reside in the xz-plane with y = 0, and (2) are black if the xz component of the point places the point on the frame and red otherwise.

Note that in this program the dimensions of Bricklayer’s virtual space and the max value of the x,y, and z components of a location are declared at the top-level of the program using val-declarations. This permits experimentation with the size of the virtual space, by changing just one number.

open Level 5; 

val dimensions = 16;
val max        = dimensions-1;

fun pictureFrame () =
    let
        val lower = 4;
        val upper = 11;
        
        fun buildPictureFrame(x,y,z) = 
            let
               val onFrame = x < lower orelse z < lower
                             orelse 
                             x > upper orelse z > upper;
            in
                if y = 0 then
                    if onFrame then BLACK else RED 
                else EMPTY
            end;
    in
        traverseWithin (0,0,0) 
                       (max,max,max) 
                       buildPictureFrame
    end;

build(dimensions,dimensions,dimensions);

pictureFrame();

show "picture frame";

ca-projection-pictureFrame

Code-along Program 2

Movie projector and blank screen

In this program we see how, through a slight modification, we can project a pictureFrame onto a slanted plane. The basic approach is as follows: We begin with an artifact (e.g., a pictureFrame) whose construction is governed by a predicate p which is parameterized on x and z. Examples of artifacts that can be constructed using such predicates include pictureFrames, chessboards, and targets. Next we define a predicate q to construct a slanted plane (e.g., y = z).

Our projection is then created using a nested conditional where the first condition determines whether a point under consideration lies on the slanted plane. This first condition, in effect, acts like a filter – it filters, from the points traversed, those points that lie on the slanted plane. The x and z values of these points lying on the slanted plane are then evaluated with respect to the (secondary) property p. The result is a projection of the artifact defined by p (e.g., the pictureFrame) onto the slanted planed defined by q.

open Level 5; 

val dimensions = 16;
val max        = dimensions-1;

fun pictureFrame () =
    let
        val lower = 4;
        val upper = 11;
        
        fun p(x,z) = x < lower orelse z < lower 
                     orelse 
                     x > upper orelse z > upper;
                     
        fun q(y,z) = y = z;
        
        fun buildPictureFrame(x,y,z) = 
            let
                val onSlantedPlane = q(y,z);
                val onFrame = p(x,z);
            in
                if onSlantedPlane then
                    if onFrame then BLACK else RED
                else EMPTY
            end;
    in
        traverseWithin (0,0,0) 
                       (max,max,max) 
                       buildPictureFrame
    end;

build(dimensions,dimensions,dimensions);

pictureFrame();

show "picture frame";

ca-projection-pictureFrame_b

Code-along Program 3

In this program we look at a 2D artifact whose construction is not predicate-based. Pixel art images are classic examples of such artifacts. The program below involves the creation of a black number 8 on a white circular surface. The original artifact is two dimensional. However, the code below expands the artifact to three dimensions. The 3D artifact created can be thought of as simply a stack of the original two dimensional artifacts. Note that this stack can be made as tall as desired. However, it should be noted that such solid artifacts contain a large number of bricks. For example, the artifact shown in this example has 7490 bricks. The number of bricks in a 3D object is an important metric to be aware of because in general LEGO Digital Designer has difficulty displaying artifacts containing more than 25K bricks.

open Level 5; 

val dimensions = 64;
val max        = dimensions-1;

fun eight (x,y,z) =
    let
        val r             = 5;
        val numTthickness = 3;
        val bgThickness   = 3*r + 1;
    in
        ringY (r*3) bgThickness 10 [WHITE] (x,y,z); 
        
        ringY r numThickness 10 [BLACK] (x+r,y,z);
        ringY r numThickness 10 [BLACK] (x-r,y,z)
    end;
   
build(dimensions,10,dimensions);

eight(30,0,30);

show "Eight";

ca-projection-8_ball_a

Code-along Program 4

The artifact created by this program is an eight ball. The artifact is hollow, but nevertheless contains 27748 bricks. The eight ball is created by first creating a stack of eights in the virtual space. This stack contains 47187 bricks. This stack artifact, which we will call the eight-stack represents an intermediate stage of the construction of the eight ball and is not displayed. A black sphere is then created in the same space where the eight-stack resides. The formula for creating a sphere is simply the extension to three dimensions of the formula used to create a circle. There are several things about how the sphere is constructed that are worth mentioning (1) the sphere is hollow, (2) all points outside of the sphere (e.g., points that may have been part of the eight-stack) are populated with EMPTY bricks, (3) on the surface of the sphere, empty cells are filled with BLACK bricks, and (4) on the surface of the sphere the IDENTITY brick is used leave non-empty cells (e.g., those that are part of the eight-stack) unchanged.

open Level 5; 

val dimensions = 64;
val max        = dimensions-1;

fun eight (x,y,z) =
    let
        val r             = 5;
        val numTthickness = 3;
        val bgThickness   = 3*r + 1;
    in
        ringY (r*3) bgThickness 10 [WHITE] (x,y,z); 
        
        ringY r numThickness 10 [BLACK] (x+r,y,z);
        ringY r numThickness 10 [BLACK] (x-r,y,z)
    end;

fun sphere radius (xCenter,yCenter,zCenter) =
  let
    fun brickFn(x,y,z) = 
      let
         val x1 = Real.fromInt (x - xCenter);
         val y1 = Real.fromInt (y - yCenter);
         val z1 = Real.fromInt (z - zCenter);
                     
         val rSquared =  x1*x1 + y1*y1 + z1*z1;
         val r = Real.round(Math.sqrt(rSquared) );                     
         val inSphere = radius >= r 
                        andalso 
                        r >= radius - 3;
      in
        if inSphere then 
            if access (x,y,z) = EMPTY then BLACK
            else IDENTITY 
        else EMPTY
      end;
  in
     traverseWithin (0,0,0) 
                    (max,max,max) 
                    brickFn
  end;

fun eightBall (x,y,z) =
    (
        eight (x,0,z);
        sphere 25 (x,y,z)
    );
    
build(dimensions,dimensions,dimensions);

eightBall(30,30,30);

show "Eightball";

ca-projection-8_ball_b Code-along Program 5

Bricklayer provides functions for creating a variety of geometric artifacts (including a function for creating a sphere). We can take advantage of this by positioning the 8 (of our 8-ball) on the xz-plane with y = 0.

ca-8_ball_c (800x518)

We then call the Bricklayer function hollowSphere to create a black (hollow) sphere directly above the 8.

ca-8_ball_c_2 (800x619)

Next, we traverse the surface of the sphere and for each cell visited we access the corresponding cell in the xz-plane where the 8 is located. If the cell in the xz-plane is empty, we leave the cell in on the sphere unchanged; otherwise we replace the brick on the sphere with the corresponding brick found on the xz-plane.

ca-8_ball_c_3

And finally, we erase the 8 in the xz-plane and we are left with the 8 ball.

ca-projection-8_ball_b

open Level_5;

val dimensions = 64;
val max        = dimensions - 1;

fun eight (x,y,z) =
    let
        val r            = 5;
        val numThickness = 3;
        val bgThickness  = 3*r + 1;
    in
        ringY (r*3) bgThickness 1 [WHITE] (x,y,z); 
    
        ringY r numThickness 1 [BLACK] (x+r,y,z);
        ringY r numThickness 1 [BLACK] (x-r,y,z)
    end;

fun brickFn (x,y,z) = 
    let
        val brickXZplane = access(x,0,z);
        val brickSphere  = access(x,y,z);            
    in
        if brickSphere = EMPTY then EMPTY
        else if brickXZplane = EMPTY then IDENTITY 
             else brickXZplane
    end;
    
fun eightBall (x,y,z) =
    (
        eight (x,0,z);
        hollowSphere 25 9 [BLACK] (x,y+1,z);
        traverseWithin (0,1,0) (max,max,max) brickFn
    );
    
build(dimensions,dimensions,dimensions);

eightBall(30,30,30);

show "Eightball";