Tutorial 6: Basic shape grammar

To access the tutorial projects in ArcGIS CityEngine, start CityEngine and click Help > Download Tutorials and Examples in the main menu. After choosing a tutorial or example, the project is automatically downloaded and added to your CityEngine workspace.

Generated buildings

This tutorial introduces the basics of the CGA shape grammar of CityEngine. You'll analyze and extend a finished rule file that contains all the steps to create a basic building.

To learn more about CGA shape grammar, see the Rule-based modeling tutorial, and the Rule-based modeling and CGA modeling help topics.

Model a simple building

To construct a simple building with a typical facade, complete the following steps:

  1. Expand the Tutorial_06_Basic_Shape_Grammar tutorial folder in the Navigator window.
  2. Open the SimpleBuilding_01.cej scene in the scenes folder to see the following building:

    Simple building with typical facade

  3. Choose the lot (or the model if it's already generated) in the Viewport, and review the information in the Inspector window.

    Inspector window

    You'll find two important parameters displayed here:

    • Rule File—Contains a link to the /rules/simpleBuilding_01.cga rule file. This rule file is run when the generation is triggered.
    • Start Rule—Defines the first rule that is run in the rule file. In this case, the start rule is the Lot rule.
  4. Open the CGA rule file by clicking the rule file link in the Inspector window or double-clicking the simpleBuilding_01.cga file in the Navigator window to open the file in the CGA Editor.

In the next sections, you'll explore the necessary attributes, assets, and rules to create a simple building.

Building attributes

Building attributes are typically defined at the beginning of the rule file (although they can be put anywhere in the rule file). These attributes are used throughout the rule set and appear below the Rule File and Start Rule definitions in the CGA Attribute Mapping Area section of the Inspector window in which their values can be set and modified outside the CGA Editor window as well.

The following are the attributes that define the building:

attr groundfloor_height = 4
attr floor_height       = 3.5
attr tile_width         = 4
attr height             = 11

Lot rule

The creation of the building starts now. The Lot rule is the first rule as assigned in the Inspector window. The mass model is created with the extrude operation:

Lot -->
    extrude(height)
    Building

Building rule

A mass model can be divided into its facades by applying the component split:

Building -->
    comp(f) { front : Frontfacade | side : Sidefacade | top: Roof }

This rule splits the building shape or mass model into its faces by applying a component split. This results in a front shape (usually the main front face with entrance), several side shapes as facades, and a roof shape.

The facades can now be modeled. In the typical facade modeling workflow, the facade is split into floors, and the floors are further split into elements called tiles (floor subdivisions). A tile typically consists of wall and window elements. This subdivision scheme can be implemented in the CGA shape grammar as follows:

Subdivision scheme

Frontfacade rule

The Frontfacade rule splits the front face into a ground floor shape with a height of 4 meters and repeats (using the repeat operator [*]) the upper floor shapes with an approximate height of 3.5 meters:

Frontfacade -->	
    split(y) { groundfloor_height : Groundfloor
             |    { ~floor_height : Floor}* }

The ~ operator guarantees that regardless of the building's actual height, a full number of upper floors is always created. The appearance of the ground floor is often different from the other floors, especially for front facades. The ground floor differs not only because of the existence of an entrance, but often also because of different-sized floor heights, the window appearance, color, and so on.

Sidefacade rule

The Sidefacade rule splits the side facades into floor shapes. The subdivision split is performed in the same way to assure that the floor heights are in sync with the front facade:

Sidefacade -->
    split(y) { groundfloor_height : Floor
             |    { ~floor_height : Floor}* }

Roof rule

The Roof rule offsets the roof shape by 0.4 meters to the inside and lowers it slightly to create a flat roof:

Roof -->
    offset(-0.4, inside)
    t(0, 0, -0.2)

Floor rule

The Floor rule is a typical example of the subdivision of a floor into tiles with an approximate width of 3 meters. To make the floor design slightly more interesting, you'll split a wall element with a width of 1 meter on each side:

Floor -->
    split(x) {           0.5 : SolidWall
             | { ~tile_width : Tile }*
             |           0.5 : SolidWall }

Groundfloor rule

The Groundfloor rule refines the ground floor shape with a similar subdivision split, with the difference that an entrance is placed on the right:

Groundfloor -->
    split(x) {           0.5 : SolidWall
             | { ~tile_width : Tile }*
             |   ~tile_width : EntranceTile
             |           0.5 : SolidWall }

The following image shows the extruded mass model, first without splits and model, then with floors and tiles splits:

Extruded mass model
Extruded mass model
Same model decomposed into floors and tiles
Mass model with floors and tiles

Tile rule

After the initial facade structure has been defined, the Tile rule can model the tiles further:

Tile -->
    split(x) {    1 : SolidWall
             | ~1.5 : split(y) { 0.4 : SolidWall | ~1.5 : Window | 0.4: SolidWall }
             |    1 : SolidWall }

The Tile rule defines the appearance of the tile by splitting along the x-axis and y-axis (with a nested split). In this design, the wall elements are floating (with ~), and the walls have a fixed width of 1 meter and a height of 0.4 meters above and below the windows.

EntranceTile rule

The EntranceTile rule defines the entrance shape in a similar way as the tile shape (but with no wall on the bottom):

EntranceTile -->
    split(x) {  ~1 : SolidWall
             | 2.5 : split(y) { 3 : Door | ~2 : SolidWall }
             |  ~1 : SolidWall }

SolidWall, Window, and Door rules

The SolidWall, Window, and Door rules add depth and complexity to the facade:

SolidWall -->
    s('1, '1, -0.4)
    primitiveCube()

Window -->
    t(0, 0, -0.2)
    split(y) { 0.1 : Frame
             |  ~1 : split(x)  {  0.1 : Frame | { ~1 : Glass | 0.1 : Frame }* }
             | 0.1 : Frame }

Door -->
    t(0, 0, -0.4)
    split(y) {   ~1 : split(x) { 0.15 : Frame 
                               |   ~1 : Panel 
                               | 0.05 : Frame 
                               |   ~1 : Panel 
                               | 0.15 : Frame }
             | 0.15 : Frame }

Using the s(x,y,z) operation , the size of the scope can be set in the SolidWall rule. The width and height of the scope are not affected since relative coordinates are used: the x- and y-dimensions of the current scope are scaled by one ('1), resulting in no change. The z-dimension is set to -0.4 meters, resulting in a wall with a thickness of 0.4 meters (pointing inward). The primitiveCube() operation creates cubic volume filling the current shape's scope.

For the Window and Door rules, the current shape is translated -0.2 and -0.4 meters, respectively, in the z-direction using the t(x,y,z) operation. This way, the windows are set back 0.2 meters into the facade, and the door is 0.4 meters back at the end of the wall. Then the shape is split in x and y to create frames and glass and panel parts.

Note that the CGA Editor window will display a warning for undefined Frame, Glass, and Panel rules. This is okay because you will add those rules in the next chapter.

By putting all of these rules together, you get the final untextured building:

Simple building with typical facade

In the next section, you'll learn how to add textures and colors to the building.

Texture and color the building

Now, you'll add textures and colors to the building by defining the textures and colors used at the top of the rule file. The textures are loaded from the assets folder.

  1. Open the SimpleBuilding_01.cej scene if it's not open.
  2. Double-click the simpleBuilding_01.cga rule file to open it.
  3. Add the following new lines to the rule file below the attributes:

    // textures & colors
    const wall_tex          = "facades/brick_white_02.jpg"
    const dirt_tex          = "dirtmaps/dirtmap_04.jpg"
    const roof_tex          = "roofs/flat_01.jpg"
    const window_color      = "#85acd6"
    const frame_color       = "#444444"
    const door_color        = "#666666"

    The textures and colors are defined as constants to not appear as rule attributes in the Inspector window.

  4. Add the setupProjection() command to the Frontfacade and Sidefacade rules:

    Frontfacade -->
        setupProjection(0, scope.xy, 1.5, 1.5, 0, 0, 1)
        setupProjection(2, scope.xy, scope.sx, scope.sy)
        split(y) { groundfloor_height : Groundfloor
                 |    { ~floor_height : Floor}* }
    
    Sidefacade -->
        setupProjection(0, scope.xy, 1.5, 1.5, 0, 0, 1)
        setupProjection(2, scope.xy, scope.sx, scope.sy)
        split(y) { groundfloor_height : Floor
                 |    { ~floor_height : Floor}* }

    The setupProjection() command prepares the UV coordinate projections on the facades for color (channel 0) and dirtmap (channel 2), projected onto the scope's x,y plane, Therefore, scope.xy is set as the second parameter.

    The brick texture (channel 0) is repeated every 1.5 meters in both the x-axis and the y-axis. The dirtmap (channel 2) spans the entire facade and uses scope.sx and scope.sy as the size parameters.

  5. Edit the Roof rule below the Sidefacade rule:

    Roof -->
        offset(-0.4, inside)
        t(0, 0, -0.2)
        setupProjection(0, scope.xy, scope.sx, scope.sy)
        texture(roof_tex)
        projectUV(0)

    The Roof rule prepares the UV coordinates to cover the entire roof face, sets the roof texture, and applies the texture coordinates to the geometry.

    Keep the building shape selected, and in the CGA Editor window, save (Ctrl+S) and generate (Ctrl+G) after every edit to see the changes.

  6. Edit the SolidWall rule with an additional component split and add a new Wall rule directly after it:

    SolidWall -->
        s('1, '1, -0.4)
        primitiveCube()
        comp(f) { side : Wall | all : setupProjection(0, scope.xy, 1.5, 1.5, 0, '1) Wall }
    
    Wall -->
        texture(wall_tex)
        set(material.dirtmap, dirt_tex)
        projectUV(0) projectUV(2)

    The component split separates the sides of the wall from the top and bottom parts. This way, a different setupProjection() can be used for those parts so the textures will be properly scaled and aligned.

    In the Wall rule, the textures for the color and dirt channels are set to the constants defined earlier. You also need to project the UVs on those two channels.

  7. Add the Glass and Frame rules after the Window rule:

    Glass -->
        color(window_color)
        set(material.specular.r, 0.5) 
        set(material.specular.g, 0.5) 
        set(material.specular.b, 0.5)
        set(material.reflectivity, 0.5) 
        set(material.shininess, 50) 
        set(material.opacity, 0.9)
    
    Frame -->
        color(frame_color)

    The color() operations set the window and frame colors to blue and dark gray, respectively. Then, you set various material properties to make the windows look more realistic.

  8. Add the Panel rule after the Door rule :

    Panel -->
        color(door_color)

    With this final step, you set the color of the door panels to a lighter gray than the frames.

    Open the simpleBuilding_02.cga rule to see the finished rule.

The following image shows the final building model:

Final building model

A close-up view of the same model:

Close-up view of the final building model

The following image shows the rule set applied to arbitrary mass models:

Rule set applied to arbitrary mass models

Add level of detail

In this section, you'll add a simple level of detail (LOD) mechanism to your building. You'll reduce the complexity (polygon count) of the model, which is helpful when creating larger areas of buildings.

To add a new LOD attribute, complete the following steps:

  1. Open the SimpleBuilding_02.cej scene.
  2. Open the simpleBuilding_02.cga file.
  3. Add the new LOD attribute below the other attributes:

    @Enum(0,1)
    attr LOD                = 1

    You'll define the following LODs:

    • LOD 0—Low level of detail, low complexity
    • LOD 1—High level of detail, high complexity

    The building you just modeled is the high-resolution model.

    Examining the current model, you'll see that you can save polygons mainly on the window assets. You'll use textured planes instead of the complex window asset. Next, you'll create a low-resolution version of the model.

    With the @Enum(0,1) annotation, you can limit the options to 0 and 1 for this attribute in the Inspector window.
  4. In the Roof rule, put the existing code below LOD > 0: and copy the texture part below else:

    Roof -->
    	case LOD > 0:
    		offset(-0.4, inside)
    		t(0, 0, -0.2)
    		setupProjection(0, scope.xy, scope.sx, scope.sy)
    		texture(roof_tex)
    		projectUV(0)
    	else:
    		setupProjection(0, scope.xy, scope.sx, scope.sy)
    		texture(roof_tex)
    		projectUV(0)

    You added a condition to the Roof rule: If the LOD value is > 0 (the high-res), use the offset and lowered roof shape. If the LOD attribute is 0, just use the simple plane shape from the initial component split.

  5. For the Window rule, keep the splitting for both conditions but apply the inset for the LOD > 0 case only:

    Window -->
        case LOD > 0:
            t(0, 0, -0.2)
            split(y) { 0.1 : Frame
                     |  ~1 : split(x) {  0.1 : Frame 
                                      | { ~1 : Glass | 0.1 : Frame }* }
                     | 0.1 : Frame }
        else:
            split(y) { 0.1 : Frame
                     |  ~1 : split(x) {  0.1 : Frame 
                                      | { ~1 : Glass | 0.1 : Frame }* }
                     | 0.1 : Frame }

  6. Do the same with the Door rule: No inset for LOD > 0:

    Door -->
        case LOD > 0:
            t(0, 0, -0.4)
            split(y) {   ~1 : split(x) { 0.15 : Frame 
                                       |   ~1 : Panel 
                                       | 0.05 : Frame 
                                       |   ~1 : Panel 
                                       | 0.15 : Frame }
                     | 0.15 : Frame }
        else:
            split(y) {   ~1 : split(x) { 0.15 : Frame 
                                       |   ~1 : Panel 
                                       | 0.05 : Frame 
                                       |   ~1 : Panel 
                                       | 0.15 : Frame }
                     | 0.15 : Frame }

  7. For the SolidWall rule, you don't need the solid elements anymore because you removed the inset of the windows and door:

    SolidWall -->
        case LOD > 0:
            s('1, '1, -0.4)
            primitiveCube()
            comp(f) { side : Wall 
                    |  all : setupProjection(0, scope.xy, 1.5, 1.5, 0, '1) Wall }
        else:
            Wall

    Save, generate, and review the attributes in the Inspector window. The new LOD attribute is shown here.

  8. Change the LOD value to 0:

    Inspector window showing attributes

    The source value changes from the rule to the value you provided. The building model generates with the LOD value set to 0.

    Low-level resolution image of building

  9. Press 5 to change the model to untextured or shaded.

    Press d-d to display the Information display bar, which displays the polygon count. With the LOD value set to 0, the model was reduced from 2,347 to 816 polygons.

    Model with LOD=1
    LOD=1 has 2,347 polygons.
    Model with LOD=0
    LOD=0 has 816 polygons.

In the next section, you'll add random variation to the buildings.

Add random variation to the attributes

Finally, you'll add variation to the generated buildings by defining random attributes:

  1. Open the SimpleBuilding_03.cej scene.
  2. Open the rule you just created, or double-click the simpleBuilding_03.cga file.
  3. Add variation to the building attributes with the following:

    attr groundfloor_height = rand(4,7)
    attr floor_height       = rand(3.5,5)
    attr tile_width         = rand(3,6)
    attr height             = rand(11,25)

    This adds a random tile width between 3 and 6 meters, a random building height between 11 and 25 meters, and random floor heights.

  4. Randomize the texture assets and the window color:

    const wall_tex          = fileRandom("facades/*.jpg")
    const dirt_tex          = fileRandom("dirtmaps/*.jpg")
    const roof_tex          = fileRandom("roofs/*.jpg")
    const window_color      = 33%  : "#85acd6"
                              33%  : "#96acb3"
                              else : "#999999"

    Using the fileRandom() function, a random texture is chosen from the respective asset subfolder for the wall, dirt, and roof texture.

    For the window color, you define three colors instead of one using a random function.

  5. To add space and variation to the lots, modify the Lot rule with the setback operation:

    Lot --> 
     setback(rand(2, 6)) { all = color("#f0f0f0") Ground. | remainder : extrude(height) Building }

    With this, you set the lot inward between 2 and 6 meters, color the outer ground shape light gray, and generate the building based on the inner shape.

  6. Since you'll generate a larger number of buildings, change the default LOD attribute value in the rule from 1 to 0 and save the rule:

    attr LOD = 0

  7. Choose the Streetnetwork layer in the Scene Editor window.
  8. Click the Generate tool Generate (Ctrl+G) to generate the buildings:

    Generated buildings

    Generated buildings from above

Open the SimpleBuilding_04.cej scene to see the final buildings with random attributes.

In this tutorial, you learned how to do the following:

  • Create the CityEngine CGA grammar elements necessary to generate a simple building.
  • Add textures, color, different levels of detail, and randomness to attributes to give variety to buildings.

To continue your learning with CityEngine, see the complete CityEngine tutorial catalog.