Tutorial 6: Basic shape grammar

To access the tutorial projects in ArcGIS CityEngine, open 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 you to 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, do the following.

  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. Select the lot (or the model if it's already generated) in the Viewport, and review the information in the Inspector window.

    Screen shot of the 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 executed when the generation is triggered.
    • Start Rule—Defines the first rule that is executed within the rule file. In this case, the start rule is the Lot rule.
  4. Open the CGA rule file by either clicking the rule file link in the Inspector 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 the 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 entire rule set and appear below the Rule File and Start Rule definition in the CGA Attribute Mapping Area of the Inspector window in which their values can be set and modified outside the CGA Editor 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 then, the floors are further broken down 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. They differ not only because 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 image below 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 height of 0.4 meter 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, Door rules

The SolidWall, Window, Door, and rules add some 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 inwards). The primitiveCube() operation creates cubic volume filling the current shape's scope.

For the Window and Door rule the current shape is translated -0.2 respectively -0.4 meters in the z-direction using the operation t(x,y,z). This way, the windows are set back 0.2 meters into the façade 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/panel parts.

Please note the CGA Editor window will show a warning for undefined Frame, Glass and Panel rules. This is ok as you will add those rules in the next chapter.

Putting all of these rules together, you'll 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 its not open.
  2. Double-click the simpleBuilding_01.cga rule file to open it.
  3. Add the following new lines to your 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 show up 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 xy 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. Whereas, the dirtmap (channel2) 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 save (Ctrl+S) and generate (Ctrl+G) after every edit to directly 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, do the following:

  1. Open the SimpleBuilding_02.cej scene.
  2. Open the simpleBuilding_02.cga file if it's not open.
  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 your high resolution model.

    Examining your 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 (your 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 do 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. Likewise, 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 and generate, then review the attributes in the Inspector window. The new LOD attribute is shown here.

  8. Change the LOD value to 0:

    Screen shot of the Inspector showing attributes

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

    Low level resolution image of building

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

    Press d-d to show the Information display bar which displays the polygon count. When the LOD value iswas set to 0, the model was reduced from 2347 to 816 polygons.

    Model with LOD=1
    LOD=1 has 2347 polygons.
    Model with LOD=0
    LOD=0 has 816 ploygons.

In the next section, you'll add random variation to your 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 your 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 to 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 different colors instead of one using a stochastic function.

  5. To get some space and variation into 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 inwards between 2 and 6 meters; coloring the outer ground shape light gray and then generating 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

    With this, you set the lot inwards between 2 and 6 meters; coloring the outer ground shape light gray and then generating the building based on the inner shape.

  7. Select 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 level of details, and randomness to attributes to give variety to buildings.

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