Tutorial 9: Advanced shape grammar

To access the tutorials in CityEngine, click Help > Download Tutorials and Examples. When you choose a tutorial or example, the project is automatically downloaded and added to your workspace.

Use complex facade patterns

This tutorial shows how to model a building from a picture and introduces some more complex CGA techniques.

Set up

Open the ComplexPatterns.cej scene if it's not already open.

Generate the building

This workflow explains how to create a set of CGA rules to re-create a building from a real-world photograph with all CGA. The facade you're going to model is shown in the following photograph. Due to the complex patterns of the tile and window layout in this example, you'll need to use some advanced CGA mechanisms such as nested repeat splits and parameter passing.

Photograph of building that will be re-created with CGA

The CGA perspective of the generated building appears as follows:

CGA perspective of the generated building

To generate the building in advance, complete the following steps:

  1. Select one of the lots in the 3D viewport.
  2. Click the generate button on the top toolbar.

Perform facade analysis

When planning a new CGA rule, it's helpful to sketch the rough layout and define some of the shape names before starting to write the rules.

The facade you'll model consists mainly of three floor types: top, ground, and upper. The lower floors are made of tiles containing windows, whereas the top floor contains only window elements. Every other lower floor is identical, so you'll pass the index (the floorIndex) with the Floor shape to create the correct look (tile and window alignment) for a specific floor.

Due to the pattern of the tiles, you'll define an intermediate DoubleTile shape, which contains two Tile shapes, and which will be helpful once you encode the floor patterns.

Complex pattern schema

Next, you'll define the detailed subshapes in a tile. It consists of two main parts: the MilkGlass and Window shapes. The Window shape contains a Blind on the top and an embedded Subwindow shape. The position of these elements depends on the horizontal position of the tile on the floor, so you need to store this position index (name it tileIndex) as a parameter of the Tile shape to be able to place the subshape structure correctly.

Detailed subshapes in a tile

Double-click the complexpatterns_01.cga file in the Navigator window to open the CGA Editor window and to see the rules that create your facade.

Attributes, variables, and assets

Attributes are defined at the beginning of the rule file. These attributes are used through the entire rule set, and you can modify them by clicking Windows > Inspector outside the CGA Grammar Editor.

// User Attribute 

@Group("Building", 1)
@Range(min=5, max=40, restricted=false) @Distance
attr buildingH = 27 // building height

@Group("Facade", 2)
@Range(min=3, max=6, restricted=false) @Distance
attr floorH = 3.5 // floor height
@Range(min=3, max=6, restricted=false) @Distance
attr groundfloorH = floorH + 1 // groundfloor height
@Range(min=1, max=4, stepsize=1, restricted=false)
attr nSymmetries = 2
@Range(min=0.1, max=1, restricted=false) @Distance
attr borderwallW = 0.3 // width of border wall stripe
@Range(min=0.1, max=0.8, restricted=false) @Distance
attr ledgeH = 0.3 // ledge height

@Group("Window", 3)
@Range(min=1, max=5, restricted=false) @Distance
attr windowW = 2.5 // window width
@Range(min=1, max=5, restricted=false) @Distance
attr milkGlassW = windowW/2 // milkglass blend width
@Range(min=0.1, max=2.5, restricted=false) @Distance
attr blindH = 0.8 // blind height
@Range(min=0.01, max=0.5, restricted=false) @Distance
attr frameW = 0.07 // frame width

@Group("Balcony", 4)
@Range(min=3, max=6, restricted=false) @Distance
attr balconyDepth = 2

@Group("Colors", 5)
@Color
attr brightblue = "#86B1C7" 
@Color
attr darkblue = "#33556C"
@Color
attr red = "#5C3F40"
@Color
attr grey ="#6B7785"
@Color
attr white = "#FFFFFF"

Add annotations such as @Group or @Range to attributes to control their appearance in the Inspector window.

Inspector window with Shape Lot 1011 rules

Other variables and assets are defined in the following block:

tileW = windowW + milkGlassW	// total tile width
const barDiameter = 0.04

// assets
const cyl_v = "general/primitives/cylinder.vert.8.notop.tex.obj"
const cyl_h = "general/primitives/cylinder.hor.8.notop.tex.obj"
const window_tex = "facade/windows/1_glass_2_blue.tif"
const milkGlass_tex = "facade/windows/blend_tex.png"

The actual creation of the building starts now. First, the mass model is created with the extrude operation. The top floor is split from the main part and split again to create the set-back balcony.

Lot --> 
	extrude(buildingH)  // Extrude the building
	split(y){ ~1: MainPart | floorH: UpperPart }  // Split top floor from lower floors

// Create a set-back by splitting in the direction of the building depth
UpperPart --> 
	split(z){ ~1: TopFloor | balconyDepth: Balcony }

Component splits are applied then to the different volume parts to distinguish the front, side, and top faces and to trigger Facade, Wall, and Roof rules.

// Create a facade on the front face, walls on the side faces, and a roof on the top face
MainPart --> 
	comp(f){ front: Facade | side: Wall | top: Roof. }  

// Create a floor (marked with -1 as top floor) on the front face, 
   walls on the side, and roof on the top face
TopFloor --> 
	comp(f){ front: Floor(-1) | side: Wall | top: Roof. }

The dimensions of the balcony are set. The railing will be placed on the faces of the current shape, so you'll use a component split to get the front, left, and right faces for the Railing rule.

// Set balcony height to 0.7 meters (railing height)
Balcony -->
	s(scope.sx-2*borderwallW,0.7,scope.sz-borderwallW)
 center(x) 
	comp(f){ front: Railing | left: Railing | right: Railing }

The rough building shape appears after volume modeling:

Rough building shape after volume modeling

Facade and floors

You'll now subdivide the front facade further. The first split subdivides the facade into a ground floor part and a set of upper floors with the help of a repeating split {...}*. The tilde sign (~) before the split size (for example, ~groundfloorH) allows a flexible height and ensures matching floors with no holes in the facade. By passing the split.index (which represents the floor index) as a parameter, you can later trigger specific floor features.

// Split the facade into a groundfloor and repeated upper floors
Facade -->
         split(y){ ~groundfloorH: Floor(split.index) 
// (all floors are marked with their split index, which represents the floor number)
                 | {     ~floorH: Floor(split.index) }* }

Every floor has a narrow wall area on its left and right borders. You'll create this with a simple split in the x-direction.

// create a narrow wall element on both sides of every floor.
Floor(floorIndex) --> 
//the floorIndex parameter is passed on to be used later
	split(x){borderwallW: Wall | ~1: FloorSub(floorIndex) | borderwallW: Wall }

Depending on the floor index, special horizontal elements are now created for every floor with horizontal split commands:

  • The upper floors only feature a top ledge.
  • The top floor has no additional elements and triggers the TileRow shape directly.
  • You'll use the floor index again in a later rule, so assign it again as a parameter with the TileRow shape.
FloorSub(floorIndex) -->	
	case floorIndex == 0:      // ground floor with index 0.    
		split(y){ 1: Wall | ~1: TileRow(floorIndex) | ledgeH: Wall}  
	case floorIndex > 0:      	// upper floors
		split(y){ ~1: TileRow(floorIndex) | ledgeH: Ledge }
	else: TileRow(floorIndex) 	// topfloor with index -1.

The facade with floor and ledge splits appears:

Facade with floor and ledge splits

Tiles

You'll now split the floors into tiles. For the top floor, there is no special pattern, only repeating window elements. To address these tiles later, you mark them with the parameter -1.

To create the special repeating pattern for the main floors, you'll create an intermediate shape named DoubleTile. To align the window elements correctly in a later step, you need the floor and the tile index (split.index), which you pass as parameters.

TileRow(floorIndex) --> 
	case floorIndex == -1:
		split(x){ ~windowW: Tile(-1) }*
// Repeating shape Tiles on the top floor, marked again with -1
	else: 
		split(x){ ~tileW*nSymmetries: DoubleTile(floorIndex,split.index) }* 
// the floor is subdivided into regular DoubleTile shapes, 
// the floor index is passed as parameter

The combination of floor and tile index determines the alignment of the windows. You therefore have two rules with repeating splits with different orders of the MilkGlass and Tile shapes.

DoubleTile(floorIndex,tileIndex) -->    
	case tileIndex%2 + floorIndex%2 == 1: 
// windows are right-aligned
		split(x){ ~milkGlassW: MilkGlass | ~windowW: Tile(tileIndex) }* 
	else:
// windows are left-aligned 
		split(x){ ~windowW: Tile(tileIndex) | ~milkGlassW: MilkGlass }*

You'll first set up the texture coordinates for the future window texture. The entire Tile shape is then split horizontally into window frames and the center part. The center part is again split, this time vertically, into frame, window, frame, blind, and frame.

Tile(tileIndex) -->
    setupProjection(0,scope.xy,scope.sx,scope.sy)
// Set up the texture coordinates for the windows
    split(x){ frameW: Frame Bracing
// This triggers the window frame as well as the bracing on the left side of the window
// the center window is split into Frame, Window, Frame, Blind, and Frame from bottom to top
            |     ~1: split(y){  frameW : Frame
                              |      ~1 : Window(tileIndex)
                              |  frameW : Frame
                              |  blindH : Blind 
// frame and bracing on the window's right side
                              |  frameW : Frame }
            | frameW: Frame Bracing }

Building split into window frames and vertical center parts forming frame, window, blind, and bracing

Windows

For the Window shape, the tile index of the DoubleTile is used to determine the position of the subwindows.

Window (tileIndex) -->

Right-aligned subwindows in the left half of the window are placed.

case tileIndex%nSymmetries >= 1:  // the Subwindows are aligned depending 
                                  // on the DoubleTile position 		
     split(x){     ~1 : Subwindow("right") 
             | frameW : Frame 
             |     ~1 : Glass     } 	// right-aligned in the left half of the window

Left-aligned subwindows in the right half of the window are placed.

case tileIndex%nSymmetries >= 0:
     split(x){     ~1: Glass
             | frameW: Frame 
// left-aligned in the right half of the window
             |     ~1: Subwindow("left") }

The tile index -1 representing the top floor windows is now used to create windows with no subwindows.

else:
	split(x){ ~1: Glass | frameW: Frame | ~1: Glass}

Using the left and right parameters, the RedWindow is placed in the correct location.

Subwindow(align) -->
	case align == "left": 
		split(x){~3: RedWindow | ~2: Glass}	  // Put the RedWindow to the left
	else: 
		split(x){~2: Glass     | ~3: RedWindow }	// And to the right otherwise

The following rule creates the frame and glass elements for the RedWindow shape:

RedWindow -->
   split(x){ frameW: RedFrame   // left...
           |     ~1: split(y){ frameW: RedFrame
                             |     ~1: RedGlass
                             | frameW: RedFrame }  // ... bottom, top ...
           | frameW: RedFrame }	// ... and right frame

RedGlass -->
   split(y){       ~1: Glass
           | frameW/2: t(0,0,-frameW) Frame
           |       ~1: t(0,0,-frameW) Glass }

The detailed window geometry appears for the RedWindow shape:

Detailed window geometry for the RedWindow shape

Add materials

Color and texture are added.

Wall --> color(darkblue)

Blind --> color(grey)

Frame --> 
	extrude(frameW) color(white)	// extrude the frame to the front

RedFrame --> 
	t(0,0,-frameW) extrude(frameW*4) color(red)

Glass --> 
	projectUV(0)  // apply texture coordinates to current shape geometry
	texture(window_tex) color(white) // and assign texture and color
	set(material.specular.r, 0.4)
 set(material.specular.g, 0.4)
 set(material.specular.b, 0.4)
	set(material.shininess, 4)
	set(material.reflectivity,0.3)
	
MilkGlass --> 
	s('1,'1,frameW*1.2) i("builtin:cube") 
	color(brightblue)
	setupProjection(0, scope.xy, scope.sx, scope.sy, 0, 0, 0)
 texture(milkGlass_tex)
 projectUV(0)
	set(material.specular.r, 0.7)
 set(material.specular.g, 0.7)
 set(material.specular.b, 0.7)
	set(material.shininess, 20)
 set(material.reflectivity,0.05)

The colored and textured model appear as follows after applying material rules:

Colored and textured model after applying material rules

Add detail elements

You'll refine the floor ledges by adding a back wall element, a cube to give it some depth, and a second thin cube that serves as a cover plate.

Ledge --> 
	Wall
	[ s('1,'0.9,0.2) i("builtin:cube") Wall ]	
	t(0,-0.1,0.2) s('1,scope.sy+0.1,0.03) i("builtin:cube") Wall

A horizontal bar is inserted to create the horizontal part of the railing. By disabling vertical trimming, the following vertical corner bars are prevented from being cut.

The vertical bars are evenly distributed with the help of a repeat split with a floating split width.

Railing --> 
	[ t(0,scope.sy-barDiameter/2,0) HBar ]
	set(trim.vertical, false)
	split(x){ ~tileW/3:  VBar }*

Cylinder assets are inserted to create the vertical and horizontal bars.

VBar --> s(barDiameter,'1,barDiameter) t(0,0,-barDiameter) i(cyl_v) color(white)
HBar --> s('1,barDiameter,barDiameter) t(0,0,-barDiameter) i(cyl_h) color(white)

The bracing of the windows consists of top and bottom mountings and a vertical bar in the middle. For the mountings, a cube is inserted, and the VBar again triggers the cylinder asset.

Bracing --> 
	s(barDiameter,'1,0.15) center(x) i("builtin:cube")
split(y){ 0.01: Wall | ~1: t(0,0,0.15) VBar | 0.01: Wall }

The final model appears with detail elements including ledges, window bracings, and railings added:

Final model with detail elements

Now that you have the final model, apply the rule on different lot shapes, or experiment with the user attributes in the Inspector window to modify your facade design.

Define styles

You can use the style keyword to define a new style that redefines some attributes. In this case, you'll create a color variation by redefining the color attributes.

@Description("A Variation in Red")
style Red_Color_Theme
attr brightblue = "#FF8080"
attr darkblue = "#D20000"
attr grey = "#ECCACA"
attr red = "#361B1B"

The Red_Color_Theme style is applied:

Building with the style keyword Red

The Candler and Parthenon CityEngine scenes below are good examples that show you the different possibilities with CGA.

Explore the Candler Building scene

  1. Open the Candler Building.cej scene.
  2. Double-click the Candler Building.cga file in the Navigator window and explore the CGA rules that create the Candler Building.

Candler Building

Explore the Parthenon temple scene

  1. Open the Parthenon.cej scene.
  2. Again, double-click the parthenon.cga file to see the rules behind the Parthenon Temple.
Parthenon temple