CityEngine Experiments

Sept 2012, Research/Fun

I've been investigating a new design software Esri CityEngine. The software allows you to generate 3D cities via rule-based, procedural modeling of buildings. Its applications are wide-ranging, from architecture and urban planning to gaming and entertainment.

It combines two of my interests - computational geometry and geographic information systems. I presented a similar project at the Grasshopper Cloud event in 2009, which explored potential applications of Grasshopper towards urban planning. Grasshopper is incredible software, but is very generalized. CityEngine is specifically made for modeling cityscapes, and its procedural modeling language, CGA, is easy to learn thanks to numerous video tutorials on the Esri web site, the built-in language reference, and downloadable example projects.

Below are some images and CGA code examples from my brief experimentation with CityEngine.

 

 

Kuler City

CityEngine allows for photo-realistic textures, but I was more interested in learning the procedural modeling language, CGA, than in making textures, so I created Kuler City. The buildings are colored using combinations found on Adobe's Kuler.com.

 

 

 

OpenStreetMap Import

You can export OSM format map files from http://www.openstreetmap.org/, and import into CityEngine. I found my current location, Clemson, SC, as seen above, then imported into CityEngine. I selected several of the city blocks bordering College Avenue, and CityEngine cleaned the imported OSM file and generated lots inside the city blocks.

 

 

 

Procedural modeling in CGA

# Extrude the lot by 60 meters.
Lot --> extrude(60)

CGA starts very simple, and then adds detail. "Lot" is the Start Rule for the building. In the above image, my first CGA rule (seen right) is applied to all of the lots. The number sign starts a comment.

 

 

 

Adding two more rules

Lot --> offset(-3) OffsetLot
OffsetLot  --> comp(f) { inside: Footprint }
Footprint --> extrude(60) 

To create a setback between the buildings, I used the offset() command. By adding the name of a rule at the end of another rule, you will send the resulting geometry to the next rule for further operations. The comp() command, retrieves specific geometries (faces, edges, or vertices) - here, I'm specifying the inside face of the offset to be Footprint. Footprint is then extruded.

 

 

 

Random Heights

attr minheight = 10
attr maxheight = 100

Lot --> offset(-3) OffsetLot
OffsetLot  --> comp(f) { inside: Footprint }
Footprint --> extrude(rand(minheight, maxheight)) Mass

Of course, cities have buildings of many heights, so we use the rand() function. I've also used attributes, which are variables that can be controlled from an interface called the Inspector, to allow users to control the building parameters (parametric modeling). Upon changing the minheight or maxheight attributes through the Inspector, the buildings will regenerate in real time.

 

 

 

Selectors

attr minheight = 10
attr maxheight = 100

Lot --> offset(-3) OffsetLot
OffsetLot  --> comp(f) { inside: Footprint }
Footprint --> extrude(rand(minheight, maxheight)) Mass

Mass -->
	comp(f) { world.south : SouthWalls }
	comp(f) { world.east  : EastWalls }
	comp(f) { world.north : NorthWalls }
	comp(f) { world.west  : WestWalls }

SouthWalls -->  color("#00ff00")
EastWalls -->   color("#ff0000")
NorthWalls -->  color("#0000ff")
WestWalls -->   color("#ff00ff")

One feature I liked was the semantic selector keywords for the comp() command. In this case, I'm sending each side of the buildings to be handled differently, based on cardinal direction. One thing we were constantly criticized for in architecture school was treating all sides of the building the same, rather than employing different environmental strategies based on sun exposure. This example is using the selectors to color all the Southern facades green, the Eastern red, and so on.

 

 

 

Creating Floors and Facades

# Get the facades and roof from the Mass.
Mass -->
 	comp(f) { side : Facade }
	comp(f) { world.up :  Roof Roof2 Roof3 Roof4}

# Split the building in the Y direction (y-up), 
# using different height attributes for different floors.
Facade -->
	split(y) { groundFloorHeight : GroundFloor
			 | {~floorHeight : Floor}*
			 | {roofFloorHeight : Floor}}

# Split the Floors in the X direction (object coordinate system).
Floor --> split(x) {1 : Corner | {~tileWidth : Tile}* | 1: Corner }

I removed the color-coding, and used the comp() function again, to get the facades and roof. I used split() to create the floors, and then again to create tiles (or bays). Note the use of the ~ and * operators; the * operator repeats an operation to fill available space, and the ~ operator means that an approximate value can be used.

I learned this from watching Esri's basic CGA shape grammar video.

 

 

 

Random Kuler color application

# Apply colors from kuler.adobe.com.
# Color functions are listed below.
GroundFloor --> color(mainColor)
Roof1 --> color("#ffffff") translate(rel, object, 0, -1, 0)
Roof2 --> color(towerColor) translate(rel, object, 0, 2, 0)
Roof3 --> color(towerColor) translate(rel, object, 0, 3, 0)
Roof4 --> color(towerColor) translate(rel, object, 0, 4, 0)
Corner --> color(mainColor)
Tile --> color (towerColor) 

# A user-controllable color selector for the Inspector.
# It is randomly selected based on the Seed.
@Range(0,10)
attr colorID = 10%:1 10%:2 10%:3 10%:4 10%:5 
			   10%:6 10%:7 10%:8 10%:9 10%:10 else:0

# The tower color is picked once per Lot,
# based colorID attribute above. 
towerColor = 
	case colorID == 0: TileColor0
	case colorID == 1: TileColor1
	case colorID == 2: TileColor2
	case colorID == 3: TileColor3
	case colorID == 4: TileColor4
	case colorID == 5: TileColor5
	case colorID == 6: TileColor6
	case colorID == 7: TileColor7
	case colorID == 8: TileColor8
	case colorID == 9: TileColor9
	case colorID == 10: TileColor10
	else : TileColor0

# The first color from each combination.
mainColor = 
	case colorID == 0: "#0F2D40"
	case colorID == 1: "#485B61"
	case colorID == 2: "#FAFAC0"
	case colorID == 3: "#A84B3A"
	case colorID == 4: "#7F6265"
	case colorID == 5: "#756E48"
	case colorID == 6: "#5D736B"
	case colorID == 7: "#467F71"
	case colorID == 8: "#011640"
	case colorID == 9: "#326B3F"
	case colorID == 10: "#FFA322"
	else : "#0F2D40"

# Define tile color combinations.
TileColor0 = 20%: "#0F2D40" 20%: "#194759" 20%: "#296B73" 20%: "#3E8C84" else: "#D8F2F0"
TileColor1 = 20%: "#485B61" 20%: "#4B8C74" 20%: "#74C476" 20%: "#A4E56D" else: "#CFFC83"
TileColor2 =  20%: "#FAFAC0" 20%: "#C4BE90" 20%: "#8C644C" 20%: "#594D37" else: "#293033"
TileColor3 =  20%: "#A84B3A" 20%: "#FF9F67" 20%: "#233138" 20%: "#FFF7F5" else: "#4C646B"
TileColor4 =  20%: "#7F6265" 20%: "#FFA256" 20%: "#F7DD77" 20%: "#E0D054" else: "#ABA73C"
TileColor5 =  20%: "#756E48" 20%: "#A37B5B" 20%: "#E0776E" 20%: "#ED9164" else: "#FFAD51"
TileColor6 =  20%: "#5D736B" 20%: "#A8BFB7" 20%: "#E3F2DF" 20%: "#384035" else: "#F27405"
TileColor7 =  20%: "#467F71" 20%: "#FFE87A" 20%: "#FFCA53" 20%: "#FF893B" else: "#E52738"
TileColor8 =  20%: "#011640" 20%: "#024059" 20%: "#F2F0D0" 20%: "#BE6C5C" else: "#8C3037"
TileColor9 =  20%: "#326B3F" 20%: "#B9D65D" 20%: "#E9FFC9" 20%: "#78D6C2" else: "#18445C"
TileColor10 =  20%: "#FFA322" 20%: "#B8390E" 20%: "#730000" 20%: "#390227" else: "#005F85"

This demonstrates the case conditional syntax, and the use of stochastic attributes (randomly determined by the Seed). I noticed file operations in the CGA reference, so I'm assuming I could pull this color data in from a spreadsheet or text file.

The translate() function was used to make several copies of the roof at different positions, three of which are gravity-defying.

 

 

 

Texturing

Roof1 --> color("#ffffff") translate(rel, object, 0, -1, 0)

Roof2 --> color(towerColor) translate(rel, object, 0, 2, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

Roof3 --> color(towerColor) translate(rel, object, 0, 3, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

Roof4 --> color(towerColor) translate(rel, object, 0, 4, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

Tile --> 
	color(towerColor)
	set(material.opacitymap,"assets/Window1Opacity.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

I'm using the opacity map to create the appearance of windows and a roof texture. I made the window texture in Photoshop (shown below), which is just a greyscale image. The darker parts of the image create more transparency.

 

 

 

 

 

 

Continue to Part 2: Nagakin Style

 

 

CGA Rules for Kuler City

Cut and paste the below code into a CityEngine rule file to try it out for yourself. Here are the two referenced files, which you must import into the assets folder in the Navigator:

/**
 * File:    rule.cga
 * Created: 1 Sep 2012 15:45:14 GMT
 * Author:  Chris
 */

version "2011.2"


# ATTRIBUTES
#
# These attributes will appear in the inspector window, 
# where the user can manipulate them parametrically and watch the building change in real time.

@Range(10,100)
attr minheight = 10

@Range(100,500)
attr maxheight = 100

attr groundFloorHeight = 4
attr floorHeight = 3
attr roofFloorHeight =1
attr tileWidth = 3


# RULES
#
# This code models the building procedurally.


# Start with Lot.
Lot --> offset(-3) OffsetLot
OffsetLot  --> comp(f) { inside: Footprint }
Footprint --> extrude(rand(minheight, maxheight)) Mass

# Get the facades and roof from the Mass.
Mass -->
 	comp(f) { side : Facade }
	comp(f) { world.up : Roof1 Roof2 Roof3 Roof4}

# Split the building in the Y direction (y-up), 
# using different height attributes for different floors.
Facade -->
	split(y) { groundFloorHeight : GroundFloor
			 | {~floorHeight : Floor}*
			 | {roofFloorHeight : RoofFloor}}

# Split the Floors in the X direction (object coordinate system).
Floor --> split(x) {1 : Corner | {~tileWidth : Tile}* | 1: Corner }
RoofFloor --> split(x) {1 : Corner | {~tileWidth : Railing}* | 1: Corner }

# Apply colors from kuler.adobe.com.
# Color functions are listed below.
GroundFloor --> color(mainColor)
Corner --> color(mainColor)
Railing --> color(mainColor)
Roof1 --> color("#ffffff") translate(rel, object, 0, -1, 0)
Roof2 --> color(towerColor) translate(rel, object, 0, 2, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)
Roof3 --> color(towerColor) translate(rel, object, 0, 3, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)
Roof4 --> color(towerColor) translate(rel, object, 0, 4, 0)
	set(material.opacitymap,"assets/RoofTexture.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

Tile --> 
	color(towerColor)
	set(material.opacitymap,"assets/Window1Opacity.jpg")
	setupProjection(0, scope.xy, '1, '1) projectUV(0)

# A user-controllable color selector for the Inspector.
# It is randomly selected based on the Seed.
@Range(0,10)
attr colorID = 10%:1 10%:2 10%:3 10%:4 10%:5 
			   10%:6 10%:7 10%:8 10%:9 10%:10 else:0

# The tower color is picked once per Lot,
# based colorID attribute above. 
towerColor = 
	case colorID == 0: TileColor0
	case colorID == 1: TileColor1
	case colorID == 2: TileColor2
	case colorID == 3: TileColor3
	case colorID == 4: TileColor4
	case colorID == 5: TileColor5
	case colorID == 6: TileColor6
	case colorID == 7: TileColor7
	case colorID == 8: TileColor8
	case colorID == 9: TileColor9
	case colorID == 10: TileColor10
	else : TileColor0

# The first color from each combination.
mainColor = 
	case colorID == 0: "#0F2D40"
	case colorID == 1: "#485B61"
	case colorID == 2: "#FAFAC0"
	case colorID == 3: "#A84B3A"
	case colorID == 4: "#7F6265"
	case colorID == 5: "#756E48"
	case colorID == 6: "#5D736B"
	case colorID == 7: "#467F71"
	case colorID == 8: "#011640"
	case colorID == 9: "#326B3F"
	case colorID == 10: "#FFA322"
	else : "#0F2D40"

# Define tile color combinations.
TileColor0 = 20%: "#0F2D40" 20%: "#194759" 20%: "#296B73" 20%: "#3E8C84" else: "#D8F2F0"
TileColor1 = 20%: "#485B61" 20%: "#4B8C74" 20%: "#74C476" 20%: "#A4E56D" else: "#CFFC83"
TileColor2 =  20%: "#FAFAC0" 20%: "#C4BE90" 20%: "#8C644C" 20%: "#594D37" else: "#293033"
TileColor3 =  20%: "#A84B3A" 20%: "#FF9F67" 20%: "#233138" 20%: "#FFF7F5" else: "#4C646B"
TileColor4 =  20%: "#7F6265" 20%: "#FFA256" 20%: "#F7DD77" 20%: "#E0D054" else: "#ABA73C"
TileColor5 =  20%: "#756E48" 20%: "#A37B5B" 20%: "#E0776E" 20%: "#ED9164" else: "#FFAD51"
TileColor6 =  20%: "#5D736B" 20%: "#A8BFB7" 20%: "#E3F2DF" 20%: "#384035" else: "#F27405"
TileColor7 =  20%: "#467F71" 20%: "#FFE87A" 20%: "#FFCA53" 20%: "#FF893B" else: "#E52738"
TileColor8 =  20%: "#011640" 20%: "#024059" 20%: "#F2F0D0" 20%: "#BE6C5C" else: "#8C3037"
TileColor9 =  20%: "#326B3F" 20%: "#B9D65D" 20%: "#E9FFC9" 20%: "#78D6C2" else: "#18445C"
TileColor10 =  20%: "#FFA322" 20%: "#B8390E" 20%: "#730000" 20%: "#390227" else: "#005F85"