Tutorial 9: Advanced shape grammar

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

Complex facade patterns

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

Tutorial setup

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

Generate the building

This tutorial explains how to create a set of CGA rules to recreate a building from a real-world photograph with pure CGA. The facade you're going to model is shown in the following photograph. Due to the tricky 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 recreated with pure CGA

GCA perspective of the generated building:

CGA perspective of the generated building

To generate the building in advance:

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

Facade analysis

When planning a new CGA rule, it's helpful to sketch the crude 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 only contains window elements. Every other lower floors 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 depend on the horizontal position of the tile on the floor, so you need to store this position index (call it tileIndex) as a parameter of the Tile shape to be able to place the subshape structure correctly.

Detailed subshapes in a tile
  1. Double-click the complexpatterns_01.cga file in the Navigator to open the CGA editor and to see the rules that create your facade.
  2. Switch to CGA textual mode, in case the visual representation opens. The buttons are on CGA editor the toolbar.

Attributes, variables, and assets

Attributes are defined at the beginning of the rule file. These attributes are used through the entire rule set and can be modified via the 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"

Annotations such as @Group or @Range are added to attributes to control their appearance in the Inspector.

Inspector window shows Shape Lot 1012 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 }

Crude building shape after volume modeling:

Crude 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 thesplit.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 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 ass 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.

Facade with floor and ledge splits:

Facade with floor and ledge splits

Tiles

You'll now split the floors into tiles. For the top floor, it's relatively easy; there is no special pattern, just 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 called 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 }

Detailed window geometry for the RedWindow shape:

Detailed window geometry for the RedWindow shape

Materials

Color and texture is 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)
 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)

Colored and textured model after applying material rules:

Image of colored and textured model after applying material rules

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 consist 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 }

Final model with detail elements including ledges, window bracings, and railings added:

Final model with detail elements

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

Different styles

Using the style keyword, a new style is defined 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 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.

Candler Building

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

Candler Building

Parthenon Temple

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