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.
Model a facade structure
This tutorial shows how to model a building from a picture and introduces some more complex CGA techniques. In this section, you'll create the basic structure of the facade with CGA rules.
Set up
To set up the tutorial, complete the following steps:
- Import the Tutorial_07_Facade_Modeling project into your CityEngine workspace.
- Open the Tutorial_07_Facade_Modeling/scenes/FacadeModeling_01.cej scene.
Facade modeling
This workflow explains how to write a set of CGA rules to re-create a facade from a real-world photograph. The facade you will model is shown in the following image:
You'll continue to analyze the photograph in more detail as you proceed with extending the rule set. You'll learn how to analyze an existing real-world facade and transfer its structure into CGA grammar rules. You'll also learn how premodeled assets can be used in CGA rules.
Create the rule file
To create the rule file, complete the following steps:
- Click New > CityEngine > CGA Rule File.
- Ensure that the container is set correctly (Tutorial_07_Facade_Modeling/rules), name the file facade_01.cga, and click Finish.
Volume and facade
Now you will begin creating the building. First, create the mass model with the extrude operation. You'll use an attribute for the building height.
- Add the attribute height to the beginning of the rule file.
- Write the starting rule using the extrude command, and name the shape Building.
- For this example, you're only interested in the facade, so use the component split to remove all faces except the front face, and then call the Frontfacade rule.
attr height = 24
Lot --> extrude(height) Building
Building --> comp(f) { front : Frontfacade}
Floors
The front facade is split horizontally into floors, each with a floor_height attribute. The Floor shape is parameterized with the split.index, which is the floor index. This parameter will be passed to subrules to determine what elements to create on specific floors.
- For the top floor, the floorindex is set to 999. This allows you to identify this floor quickly.
- Define some attributes for the floor dimensions.
- Add the rule.
- Select the lot in the 3D viewport.
- Click Shapes > Assign Rule File, select the facade_01.cga rule file, and click OK.
- Click the generate button on the top toolbar (or press Ctrl+G).
attr groundfloor_height = 5.5
attr floor_height = 4.5
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
Floor ledges
Floors are now split into ledge and tile shapes. Bottom ledges are specific to floors, so you'll use the floorindex parameter for this rule.
- The ground floor (floorindex 0) has no ledges and therefore calls Tiles only.
- Because windows start at floor level, there is no bottom ledge for the second floor. The balcony on this floor will be created in a later step.
- All other floors have bottom ledge, tile, and top ledge areas.
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}
Subfloors
Subfloors consist of small wall areas on left and right edges and repeating tiles in between.
- Add the following attribute on top:
- Add the rule:
- Dark bricks with dirt texture
- Bright bricks with dirt texture
- Dirt texture only—This is mainly needed for facade assets that do not have a brick structure.
attr tile_width = 3.1
Subfloor(floorindex) -->
split(x){ 0.5 : Wall(1)
| { ~tile_width : Tile(floorindex) }*
| 0.5 : Wall(1) }
attr wallColor = "#ffffff"
Wall(walltype) -->
// dark bricks with dirt
case walltype == 1 :
color(wallColor)
// bright bricks with dirt
case walltype == 2 :
color(wallColor)
// dirt only
else :
color(wallColor)
Tiles
Tiles are homogeneous on this facade. You only need to differentiate between the ground floor tiles and the upper floors.
Use the door_width and window_width attributes to set the split dimensions.attr door_width = 2.1
attr window_width = 1.4
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 was added. This is necessary because doors on the ground floor are inset from the facade. To avoid holes between door and wall, you'll use a solid wall element there by inserting a cube with a defined thickness. Because you'll use this thickness again in a later Door rule, you define it as a const variable wall_inset. Declaring values that are used more than once is a good practice, as it ensures that the same value is used in different rules.const wall_inset = 0.4
SolidWall -->
s('1,'1,wall_inset) t(0,0,-wall_inset)
i("builtin:cube:notex")
Wall(1)
Declare a function to get the wall type from the floor index. Looking at the facade, you 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.getWalltype(floorindex) =
case floorindex == 0 : 1
case floorindex == 1 : 1
else : 2
Insert facade assets
Next, you'll learn how to use premodeled assets on the facade.
- Open the Tutorial_07_Facade_Modeling/scenes/FacadeModeling_02.cej scene file if it's not already open.
- Open the Tutorial_07_Facade_Modeling/rules/facade_02.cga file.
Assets
Looking at the photograph of the facade you're going to model, 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
These assets are already present in the assets folder of the tutorial project. You can preview these assets in the CityEngine Inspector window by selecting them in the Navigator window.
Asset declarations
It's a good practice to keep all the asset declarations in the same place. Add the following lines below the attribute declarations to 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"
Windows
- You have the rules ready for the exact placement of the window assets. Call the Window shape in the WindowTile rule.
- Add the following rule to scale, position, and insert the window asset and a glass plane behind it:
WindowTile(floorindex) --> Window
Window -->
s('1,'1,0.2) t(0,0,-0.2)
t(0,0,0.02)
[ i(window_asset) Wall(0) ]
Glass
Window ornaments
Looking at the facade photo again, you notice there are different windows (or window elements) on the different floors.
You need to extend the WindowTile rule and trigger shapes specific to the floor index as follows:
- There are no special ornaments on the first and top floors (indices 1 and 999); consequently, only Window is invoked.
- On the second floor, you'll insert an additional shape, WindowOrnamentRound. 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.
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
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.
Set the dimensions, center the scope on the x-axis, insert the cube, and color it for better visibility.WindowOrnamentTriangle -->
s('1.7, 1.2, 0.3) center(x) i("builtin:cube") color("#ff0000")
# set dimensions for the triangle window element and insert it
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")
Exchange proxies for real assets
The dimensions of the window ornaments seem reasonable, so you can insert the assets instead of the cube (for the WindowLedge, keep it with the cube).
WindowOrnamentTriangle -->
s('1.7, 1.2, 0.3) center(x) i(tri_wintop_asset)
WindowOrnamentRound -->
s('1.7, 1.2, 0.4) center(x) i(round_wintop_asset)
On the second floor, the round window ornaments are missing the side pillars. You need to add a new split command to the WindowOrnamentRound rule you previously added. This line will prepare 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 }
Dimensions are set, and the modillion asset is inserted. 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.
WindowMod -->
s(0.2,'1.3,'0.6) t(0,'-1,0) center(x) i(modillion_asset) Wall(0)
Doors
A door tile is split vertically into door, arc, and arctop areas.
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}
On the top area, a wall element and an overlayed modillion asset are inserted.
# Adds wall material and a centered modillion
Arctop -->
Wall(1)
s(0.5,'1,0.3) center(x) i(modillion_asset) Wall(1)
The arcs area is split again, and two arc assets are inserted. Use the wall_inset variable you defined earlier. You need to rotate the right halfarc asset 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}
Set the Wall shape on Doortop and Door, and insert the actual arc asset.
Doortop --> Wall(0)
Door --> t(0,0,-wall_inset) Wall(0)
ArcAsset --> i(halfarc_asset) Wall(1)
Ledges
For the ledges, you need rules for top and bottom ledges. Top ledges use a simple wall stripe, and bottom ledges must be distinguished on the different floors with the ledge asset inserted.
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)
Balcony
Now you'll work on the balcony.
- Use the Floor rule, and add the Balcony shape to the second floor case.
- Start with a simple proxy to ensure the placement and dimensions of the balcony.
- The balcony box is now split into its components: beams, floor, and railing.
- Create the beams supporting the balcony with a repeating split.
- Using the component split on the RailingBox, extract the necessary faces for the balcony railings.
- Set the dimension insert to a cube to create the balcony rails.
case floorindex == 2 :
split(y){~1 : Subfloor(floorindex) Balcony | 0.5 : TopLedge}
Balcony -->
s('1,2,1) t(0,-0.3,0) i("builtin:cube") color("#99ff55")
Balcony -->
s('1,2,1) t(0,-0.3,0) i("builtin:cube")
split(y){0.2 : BalconyBeams
| 0.3 : BalconyFloor
| 1 : RailingBox }
BalconyBeams -->
split(x){ ~0.4 : s(0.2,'1,'0.9) center(x) Wall(0) }*
BalconyFloor --> Wall(0)
# Get the front, left, and right components (faces) of the RailingBox shape
RailingBox -->
comp(f){front : Rail | left : Rail | right : Rail}
Rail -->
s('1.1,'1,0.1) t(0,0,-0.1) center(x) i("builtin:cube") Wall(0)
Final facade with geometry assets
Now you have the final model. Apply the rule to different lot shapes, or explore the user attributes in the Inspector window to modify the facade design.
In the next section, you'll learn how to apply textures to this facade.
Texture the facade
Use the following techniques to texture the facade:
- Open the Tutorial_07_Facade_Modeling/scenes/FacadeModeling_03.cej scene file if it's not already open.
- Open the Tutorial_07_Facade_Modeling/rules/facade_03.cga file.
Texture assets
- On top of the rule file, add the textures that you want to use as attributes.
- For the window and door textures, use a function to get the texture string so you don't need to list the textures separately.
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"
randomWindowTex = fileRandom("*facades/textures/window.*.jpg")
randomDoorTex = fileRandom("*facades/textures/doortex.*.jpg")
Set up the global UV coordinates
Texturing with the shape grammar consists of the following three commands:
- setupProjection()—Defines the UV coordinate space
- set(material.map—Sets a texture file
- projectUV()—Applies the UV coordinates
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.
- Change the Building rule to the following:
- Create the following new rule:
- Generate the facade to see the UV setup.
- Add the UV setup for the dirt channel.
- Generate the facade again to get the following result:
- To see how the facade will look with the textures, exchange the built-in uvtest texture with the real ones.
- For the building, you only need the UV's setup at this point, so change the FrontfacadeTex rule to the following:
Building --> comp(f) { front : FrontfacadeTex}
FrontfacadeTex -->
setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
setupProjection(2, scope.xy, '1, '1)
Frontfacade
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)
FrontfacadeTex -->
setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
texture(wall_tex)
projectUV(0)
setupssProjection(2, scope.xy, '1, '1)
set(material.dirtmap, dirt_tex)
projectUV(2)
FrontfacadeTex -->
setupProjection(0, scope.xy, 2.25, 1.5, 0, 0, 1)
setupProjection(2, scope.xy, '1, '1)
Frontfacade
Texture the walls
Previously in this workflow, you added a type parameter to the Wall rule. You'll use that now to get three wall types with different textures.
- Change the Wall rule 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)
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.
- Call the randomWindowTex function you defined earlier for the texture call.
- Add specular luster to the glass.
Glass -->
setupProjection(0,scope.xy, '1, '1)
projectUV(0)
texture(randomWindowTex)
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)
Texture the door shapes
The door planes are textured in the same way as the window glass.
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)