Tutorial 7: Facade modeling

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.

Textured window assets and door shapes

In this tutorial, you'll learn how to model a building from a picture, including creating a facade structure, inserting assets, and adding textures to the building. While doing this, you'll explore some complex CGA techniques.

Model a facade structure

The following workflow shows you how to write a set of CGA rules to re-create a facade from the real-world photograph below:

Photo of the facade to be modeled

In this section, you'll create the basic structure of the facade with CGA rules. You'll continue to analyze the photograph in more detail as you proceed with extending the rule set, and you'll also learn how premodeled assets can be used in CGA rules.

Create the rule file

To create the rule file, do the following steps:

  1. Expand the Tutorial_07_Facade_Modeling tutorial folder in the Navigator window.
  2. Open the FacadeModeling_01.cej scene in the scenes folder.
  3. Click New > CityEngine > CGA Rule File.
  4. Click Next.
  5. Name the rule facade_01.cga.
  6. Click Finish.

    A new CGA file is created, and the CGA Editor window appears. It's empty except for the version statement with the current version of CityEngine.

Volume and facade

Now begin creating the building. First, create the mass model with the extrude operation. Use an attribute for the building height.

  1. Add the height attribute to the beginning of the rule file:

    attr height = 24

  2. Write the starting Lot rule using the extrude operation, and name the shape Building:

    Lot -->
    	extrude(height)
    	Building

  3. Since you're only interested in the facade, use a component split in the Building rule to remove all faces except the front face, and call the Frontfacade rule:

    Building -->
    	comp(f) { front : Frontfacade }

Add floors

Looking to the photo of the facade above, you can analyze the different types of floors as shown in the following image:

Facade split horizontally into floors

  1. Define the attributes for the floor dimensions after the height attribute:

    attr groundfloor_height  = 5.5
    attr floor_height        = 4.5

  2. Add the Frontfacade rule below the Building rule:

    Frontfacade -->	
    	split(y) { groundfloor_height : Floor(split.index)          // Groundfloor
    		 |       floor_height : Floor(split.index)          // First Floor
    		 |       floor_height : Floor(split.index)          // Second Floor
    		 |    { ~floor_height : Floor(split.index) }*       // Mid Floors
    		 |       floor_height : Floor(999)                  // Top Floor, indexed with 999
    		 |                0.5 : s('1, '1, 0.3) LedgeAsset } // The top ledge just below the roof

    The front facade is split horizontally into floors, each with a floor_height attribute. The Floor shape is parameterized with the split.index shape attribute, which is the floor index. This parameter is passed to subrules to determine what elements are needed to create for specific floors:

    The floor index is set to 999 for the top floor. This allows you to identify this floor. Note the repeating split for the mid floors. This allows the building to adapt dynamically to different heights and fill the remaining vertical space with mid floors.

    Note, the CGA Editor window will display a warning for the undefined Floor and LedgeAsset rules. This is okay because you will reference the rules later in the tutorial.

  3. Press Ctrl+S to save the rule file.
  4. Drag the facade_01.cga rule in the Navigator window onto the lot in the Viewport window.

    This generates the facade for the first time:

    Generated facade

    You can press the Z key to navigate to the front of the facade.

    To assign the rule and generate the model another way, click Assign in the Inspector window, choose the facade_01.cga rule, and click Generate Generate (Ctrl+G).

Add floor ledges

The floors are now split into ledge and tile shapes.

Facade showing floor ledges

  1. Add the Floor rule with the floorindex parameter:

    Floor(floorindex) -->
    	case floorindex == 0 :
    		Subfloor(floorindex)
    	case floorindex == 2 :
    		split(y) {  ~1 : Subfloor(floorindex)
    			 | 0.5 : TopLedge }
    	else :
    		split(y) {   1 : BottomLedge(floorindex)
    			 |  ~1 : Subfloor(floorindex) 
    			 | 0.5 : TopLedge }

    The floor index is used to handle the specific ledges for the different floors:

    • The ground floor (floorindex 0) has no ledges and therefore calls tiles only.
    • The windows start at floor level, so there is not a bottom ledge for the second floor. The balcony on this floor is created in a later step.
    • All other floors have bottom ledge, tile, and top ledge areas.

  2. Save the rule file and generate:

    Generated floor ledges

Add subfloors

Subfloors consist of small wall areas on the left and right edges, and repeating tiles between them:

Facade showing subfloors
  1. Add the following attribute at the top of the rule file with the other attributes:

    attr tile_width = 3.1

  2. Add the Subfloor rule at the bottom of the rule file:

    Subfloor(floorindex) -->
    	split(x) {           0.5 : Wall(1)
    		 | { ~tile_width : Tile(floorindex) }*
    		 |           0.5 : Wall(1) }

    This splits the floor horizontally into repetitive tiles and walls on both sides.

  3. Add the wallColor attribute next to the other attributes:

    attr wallColor = "#ffffff"

  4. Continue at the bottom of the rule file by adding the Wall rule with the walltype parameter:

    Wall(walltype) -->
    	case walltype == 1 :
    		color(wallColor)
    	case walltype == 2 :
    		color(wallColor)
    	else :
    		color(wallColor)

    A parameterized Wall shape is added. This is important when you'll texture the facade in a later step. In the facade photograph, there are three wall types:

    • Dark bricks with dirt texture
    • Bright bricks with dirt texture
    • Dirt texture only—This is for facade assets that do not have a brick structure.

  5. Save and generate:
    Generated subfloors

    As you can see, for now the wall styles in the Wall rule above produce identical output. As mentioned before, this will change when you add different textures to the wall types later.

Add tiles

Tiles are homogeneous in the facade. You only need to differentiate between the ground floor tiles and the upper floors.

Facade showing tiles

  1. Define the door_width and window_width attributes to set the split dimensions:

    attr door_width   = 2.1
    attr window_width = 1.4

  2. Add the Tile rule after the Subfloor rule:

    Tile(floorindex) -->
    	case floorindex == 0 :
    		split(x) {           ~1 : SolidWall 
    			 |   door_width : DoorTile
    			 |           ~1 : SolidWall }
    	else :
    		split(x) {           ~1 : Wall(getWalltype(floorindex))
    			 | window_width : WindowTile(floorindex)
    			 |           ~1 : Wall(getWalltype(floorindex)) }

    For the ground floor tiles, a new SolidWall shape is added instead of the Wall shape. This is necessary because doors on the ground facade floor are inset from the facade. To avoid holes between doors and walls, you'll use a solid wall element by inserting a cube with a defined thickness. Because you'll use this thickness again later in the Door rule, you'll define it as a const variable wall_inset.

    Declaring values that are used more than once is good practice, as it ensures that the same value is used in different rules.

  3. Add the wall_inset const between the attributes and the Lot rule:

    const wall_inset = 0.4

  4. Add the SolidWall rule below the Wall rule:

    SolidWall -->
    	s('1, '1, wall_inset)
    	t(0, 0, -wall_inset)
    	i("builtin:cube:notex")
    	Wall(1)

  5. Declare a function above the Wall rule to get the wall type from the floor index:

    getWalltype(floorindex) =
    	case floorindex == 0 : 1
    	case floorindex == 1 : 1
    	else : 2

    Looking at the facade photograph, you can see that there are dark textures on the ground and first floor, and bright textures on the others. The getWalltype function maps the floor index to the corresponding wall type.

  6. Save and generate:

    Generated tiles

To see what the scene and rule look like at this point, open the FacadeModeling_02.cej scene and facade_02.cga rule file.

Insert facade assets

Next, you'll learn how to use premodeled assets on the facade.

Assets

Looking at the photo of the facade again, you see you need the following assets:

  • Window—Used for the window elements
  • Round Windowtop—Used for the ornaments above windows
  • Triangle Windowtop—Used for the ornaments above windows
  • Half arc—Used for arcs on the ground floor
  • Ledge—Used for all ledges
  • Modillion—Used for window ornaments and arc ornaments on the ground floor
Required assets

These assets are already present in the assets folder of the tutorial project. To preview these assets, double-click them in the Navigator window or select them and right-click and choose File Preview.

Preview of the triangle windowtop asset

Add references to the assets below the attribute declarations in the rule file:

const window_asset       = "facades/elem.window.frame.obj"
const round_wintop_asset = "facades/round_windowtop.obj"
const tri_wintop_asset   = "facades/triangle_windowtop.obj"
const halfarc_asset      = "facades/arc_thin.obj"
const ledge_asset        = "facades/ledge.03.twopart_lessprojection.obj"
const modillion_asset    = "facades/ledge_modillion.03.for_cornice_ledge_closed.lod0.obj"

Add windows

To add windows, complete the following steps:

  1. You have the rules ready for the exact placement of the window assets. Call the Window shape in the WindowTile rule by adding it after the Tile rule:

    WindowTile(floorindex) -->
    	Window

  2. Next, add the Window rule to scale, position, and insert the window asset and a glass plane behind it:

    Window -->
    	s('1, '1, 0.2)
    	t(0, 0, -0.18)
    	[ i(window_asset) Wall(0) ]
    	Glass

  3. Save and generate:

    Generated window asset

Add window ornaments

Looking at the facade photo again, you'll notice there are different windows (or window elements) on the different floors:

Facade showing window ornaments
  1. Extend the WindowTile rule and trigger shapes specific to the floor index as follows:

    WindowTile(floorindex) -->
    	case floorindex == 1 || floorindex == 999 : 
    		Window
    	case floorindex == 2 : 
    		Window
    		t(0, '1, 0)
    		WindowOrnamentRound
    	else :
    		Window
    		WindowLedge
    		t(0, '1, 0)
    		WindowOrnamentTriangle

    • There are no special ornaments on the first and top floors (indices 1 and 999); consequently, only the Window shape is invoked.
    • On the second floor, you'll insert the WindowOrnamentRound element as an additional shape. Because this element is to be aligned to the top border of the window, you translate the current scope upward along the y-axis with '1.
    • The other window tiles (on the mid floors) get an additional WindowLedge shape, as well as the WindowOrnamentTriangle ornament, again translated along the y-axis.

    Rather than using the final assets directly, you'll insert proxy cubes first. This makes it easier to set the dimensions for the real assets. You can use the built-in cube asset for this case.

  2. Add the following rules after the WindowTile rule to create the proxy cubes by setting the dimensions, centering the scope on the x-axis, inserting the cube, and coloring them for better visibility.

    WindowOrnamentTriangle -->
    	s('1.7, 1.2, 0.3)
    	center(x) 
    	i("builtin:cube")
    	color("#ff0000")
    
    WindowOrnamentRound -->
    	s('1.7, 1.2, 0.4)
    	center(x)
    	i("builtin:cube")
    	color("#00ff00")
    
    WindowLedge -->
    	s('1.5, 0.2, 0.1)
    	t(0, -0.2, 0)
    	center(x)
    	i("builtin:cube")
    	color("#0000ff")

  3. Save and generate:

    Generated window ornaments

    Close-up view of generated window ornaments

Exchange proxies for real assets

To exchange proxies for real assets, complete the following steps:

  1. The dimensions of the window ornaments seem reasonable, so you can now insert the assets instead of the cube (for the WindowLedge, keep it with the cube), and replace the color with a call to the Wall shape for each of the rules:

    WindowOrnamentTriangle -->
    	s('1.7, 1.2, 0.3)
    	center(x) 
    	i(tri_wintop_asset)
    	Wall(0)
    
    WindowOrnamentRound -->
    	s('1.7, 1.2, 0.4)
    	center(x)
    	i(round_wintop_asset)
    	Wall(0)
    
    WindowLedge -->
    	s('1.5, 0.2, 0.1)
    	t(0, -0.2, 0)
    	center(x)
    	i("builtin:cube")
    	Wall(0)

  2. Save and generate:

    Generated window ornament triangle and window ornament round

    Close-up view of window ornament triangle and window ornament round

  3. Since the round window ornaments are missing the side pillars on the second floor, extend the WindowOrnamentRound rule with a split operation. This prepares the scopes for the following modillion asset:

    WindowOrnamentRound -->
    	s('1.7, 1.2, 0.4)
    	center(x)
    	i(round_wintop_asset)
    	Wall(0)
    	split(x) {           ~1 : WindowMod
    		 | window_width : NIL
    		 |           ~1 : WindowMod }

  4. Add a WindowMod rule after the WindowOrnamentRound rule. The dimensions are set before inserting the modillion:
    WindowMod -->
    	s(0.2, '1.3, '0.6)
    	t(0, '-1, 0)
    	center(x)
    	i(modillion_asset)
    	Wall(0)

    Note, that by applying the relative negative translation ('-1) in the y-direction, the asset's top is aligned to the bottom face of the ornament.

  5. Save and generate:

    Generated window ornament round with side pillars

Add doors

A door tile is split vertically into the door, arc, and arctop areas.

Facade showing door tiles

Continue by adding the following rules below the Window rule.

  1. To ensure nonelliptical arcs, the height of the arcs area needs to be half the width of the door (the current x-scope):

    DoorTile -->
    	split(y) {         ~1 : Door
    		 | scope.sx/2 : Arcs
    		 |        0.5 : Arctop }

  2. On the top area of the door, a wall element and an overlayed modillion asset are inserted.

    Arctop -->
    	Wall(1)
    	s(0.5, '1, 0.3)
    	center(x)
    	i(modillion_asset)
    	Wall(1)

  3. The arcs area is split again, and two arc assets are inserted. Use the wall_inset variable you defined earlier for the inset. You also need to rotate the right halfarc asset before inserting to orient it correctly:

    Arcs -->
    	s('1, '1, wall_inset)
    	t(0, 0, -wall_inset)
    	Doortop
    	i("builtin:cube")
    	split(x) { ~1 : ArcAsset
    		 | ~1 : r(scopeCenter,0,0,-90) ArcAsset }

  4. Insert the actual arc asset in the ArcAsset rule and set the Wall type along with the Doortop and Door rules:

    ArcAsset --> 
    	i(halfarc_asset)
    	Wall(1)
    
    Doortop -->
    	Wall(0)
    
    Door -->
    	t(0,0,-wall_inset)
    	Wall(0)

    The door is set back from the wall in the Door rule.

  5. Save and generate:

    Generated doors

Add ledges

Top ledges use a simple wall stripe, and bottom ledges must be distinguished on the different floors with the ledge asset inserted.

  1. Add rules for top and bottom ledges:

    TopLedge -->
    	WallStripe
    
    BottomLedge(floorindex) -->
    	case floorindex == 1 :
    		split(y) { ~1 : Wall(0)
    			 | ~1 : s('1, '1, 0.2) LedgeAsset }
    	case floorindex == 999 :
    		split(y) { ~1 : WallStripe
    			 | ~1 : s('1, '1, 0.2) LedgeAsset }
    	else : WallStripe
    
    WallStripe -->
    	split(x) { 0.5 : Wall(1)
    		 |  ~1 : Wall(2)
    		 | 0.5 : Wall(1) }
    
    LedgeAsset -->
    	i(ledge_asset)
    	Wall(0)

  2. Save and generate:

    Generated ledges

    Close-up view of generated ledges

Add balcony

Now you'll work on the balcony.

  1. Edit the Floor rule, and add the Balcony shape to the second floor case:

    case floorindex == 2 :
    		split(y) {  ~1 : Subfloor(floorindex) Balcony 
    			 | 0.5 : TopLedge }

  2. Start with a simple proxy with color added to ensure the placement and dimensions of the balcony by adding the Balcony rule after the LedgeAsset rule:

    Balcony -->
    	s('1, 2, 1)
    	t(0, -0.3, 0)
    	i("builtin:cube")
    	color("#99ff55")

  3. Save and generate:

    Generated balcony

  4. Split the balcony box into its components: beams, floor, and railing as in the image below:

    Showing facade balcony beams, floor, and railing box

    1. Replace the green color in the Balcony rule by splitting the balcony into the BalconyBeams, BalconyFloor, and RailingBox elements:
      Balcony -->
      	s('1, 2, 1)
      	t(0, -0.3, 0)
      	i("builtin:cube")
      	split(y) { 0.2 : BalconyBeams
      		 | 0.3 : BalconyFloor
      		 |   1 : RailingBox }
    2. Save and generate:

      Balcony showing beams, floor, and railing box

  5. Create the beams supporting the balcony with a repeating split:

    BalconyBeams -->
    	split(x) { ~0.4 : s(0.2, '1, '0.9) center(x) Wall(0) }*

    The BalconyFloor shape only defines the wall type.

    BalconyFloor -->
    	Wall(0)

  6. Using a component split on the RailingBox rule, extract the necessary faces for the balcony railings:

    RailingBox -->
    	comp(f) { front : Rail | left : Rail | right : Rail }

  7. To set the dimension of the balcony box, insert a cube to create the balcony rails:

    Rail -->
    	s('1.1, '1, 0.1)
    	t(0, 0, -0.1)
    	center(x)
    	i("builtin:cube")
    	Wall(0)

  8. Save and generate:

    Facade with wireframe
    Rendered facade without wireframe

    Now you have the final model with the geometry assets placed. You can apply your rule to different lots, or explore the user attributes in the Inspector window to modify the facade design.

Open theFacadeModeling_03.cej scene and facade_03.cga rule for the final results.

In the next section, you'll learn how to apply textures to the facade.

Texture the facade

Now, you'll apply textures to the facade.

Create texture assets

To create texture assets, complete the following steps:

  1. At the top of the rule file after the assets definition, add the textures that you want to use:

    const wall_tex    = "facades/textures/brickwall.jpg"
    const wall2_tex   = "facades/textures/brickwall_bright.jpg"
    const dirt_tex    = "facades/textures/dirtmap.15.tif"
    const doortop_tex = "facades/textures/doortoptex.jpg"

  2. For the window and door textures, define a function to get a random texture string from the respective asset folder so you don't need to list the textures separately. Add the following lines after the textures:

    randomWindowTex = fileRandom("*facades/textures/window.*.jpg")
    randomDoorTex   = fileRandom("*facades/textures/doortex.*.jpg")

Set up the global UV coordinates

Texturing with CGA is done in the following three steps:

  1. setupProjection()—Defines the UV coordinate space.
  2. set(material.map) or texture()—Sets a texture file.
  3. projectUV()—Applies the UV coordinates.

Add texture layers

You'll add two texture layers to the facade: a brick texture and a dirt map. To maintain consistent texture coordinates over the entire facade, you need to add the UV setup to the Facade rule. To test the texturing setup beforehand, you'll add a new intermediate FrontfacadeTex rule.

  1. Change the Building rule to the following:

    Building -->
    	comp(f) { front : FrontfacadeTex }

  2. Create the new FrontfacadeTex rule after the Building rule:

    FrontfacadeTex -->
    	setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
    	texture("builtin:uvtest.png")
    	projectUV(0)

    setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1) defines the texture coordinates for the texture channel 0 (the color channel). The UV coordinates are projected along the scope's x, y plane and repeated every 2.25 units in the x-direction and every 1.5 units in the y-direction. The texture() operation is a shortcut for set(material.map). In this case, it sets the color map to builtin:uvtest.png, which is a texture to quickly check UV coordinates. Then apply the UV coordinates by baking the UV coordinates for channel 0:

  3. Save and generate the facade to see the UV setup:

    Facade UV setup

  4. Add the UV setup for the dirt channel:

    FrontfacadeTex -->
    	setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
    	texture("builtin:uvtest.png")
    	projectUV(0)
    
    	setupProjection(2, scope.xy, '1, '1)
    	set(material.dirtmap, ("builtin:uvtest.png"))
    	projectUV(2)

    This texture should span the entire facade, so you'll use the relative operators '1 and '1 for the UV setup, which are the dimensions of the facade.

  5. Save and generate:

    Generated facade UV setup

  6. To see how the facade will look with the textures, exchange the builtin:uvtest.png texture with the real ones.

    FrontfacadeTex -->
    	setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
    	texture(wall_tex)
    	projectUV(0)
    
    	setupProjection(2, scope.xy, '1, '1)
    	set(material.dirtmap, dirt_tex)
    	projectUV(2)

    Facade textures

    The UV coordinates are appropriate for the facade.

  7. For the building, you only need the UV's setup at this point, so change the FrontfacadeTex rule to the following:

    FrontfacadeTex -->
    	setupProjection(0, scope.xy, 2.25, 1.5,  0, 0, 1)
    	setupProjection(2, scope.xy, '1, '1)
    	Frontfacade

    The Frontfacade rule now has UV coordinates set up correctly for the subsequent elements.

    See Texturing: Essential knowledge in the CGA Reference for more information.

Texture the walls

Earlier in this workflow, you added walltype parameter to the Wall rule. You'll use that now to assign different textures for each wall type.

  1. Look for the Wall rule and change it to the following:

    Wall(walltype) -->
    	// dark bricks with dirt
    	case walltype == 1 :
    		color(wallColor)
    		texture(wall_tex)
    		set(material.dirtmap, dirt_tex)
    		projectUV(0) projectUV(2)
    	// bright bricks with dirt
    	case walltype == 2 :
    		color(wallColor)
    		texture(wall2_tex)
    		set(material.dirtmap, dirt_tex)
    		projectUV(0) projectUV(2)
    	// dirt only
    	else :
    		color(wallColor)
    		set(material.dirtmap, dirt_tex)
    		projectUV(2)

  2. Save and generate:

    Textured walls

    All wall elements are textured with one of the wall types.

Texture the window asset

For the window asset, you'll use a set of window textures to color the glass pane, so you need to set up the UV coordinates to span the entire glass shape. To do this, use '1 for both the x- and y-directions. In the texture operation, a random glass texture gets chosen by calling the randomWindowTex function you defined earlier.

  1. Add the Glass rule after the Window rule:

    Glass -->
    	setupProjection(0, scope.xy, '1, '1)
    	projectUV(0)
    	texture(randomWindowTex)

  2. Save and generate:

    Textured windows

  3. Now, add specular luster to the glass.

    Glass -->
    	setupProjection(0, scope.xy, '1, '1)
    	projectUV(0)
    	texture(randomWindowTex)
    	set(material.specular.r, 1)
    	set(material.specular.g, 1)
    	set(material.specular.b, 1)
    	set(material.shininess, 4)

  4. Save and generate:

    Glass textured with luster window

Texture the door shapes

The door planes are textured in the same way as the window glass. Replace the Doortop and Door rules with the following lines:

Doortop -->
	setupProjection(0, scope.xy, '1, '1)
	texture(doortop_tex)
	projectUV(0)

Door -->
	t(0, 0, -wall_inset)
	setupProjection(0, scope.xy, '1, '1)
	texture(randomDoorTex)
	projectUV(0)

Textured window assets and door shapes

Open the FacadeModeling_04.cej scene and facade_04.cga rule to see the final results.

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

  • Model the structure of a facade.
  • Insert assets such as windows, doors, and ledges.
  • Apply textures to the walls, windows, and doors.

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

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