The missing ultimate guide to PBR texturing in CityEngine

1423
2
07-09-2021 09:43 AM
nicolaisteino
New Contributor II

Hi there,

WARNING: Long post

I have spent quite some time trying to get my head around all the intricacies of PBR texturing in CityEngine. As I have failed to find the ultimate guide to PBR texturing in CityEngine, I'd be grateful if someone would make it. Here are some of the things that I find troublesome, with some of my considerations below:

  1. Setting up projection for all relevant texture maps at the most strategic place in the script
  2. Dealing with textures that do not wrap properly on some sides of extruded volumes
  3. Mapping different types of PBR maps to the appropriate material shape attributes in CE
  4. Organising my texturing script in the most efficient way

Projection

First of all: It seems that textures can be applied just fine without setting up projection at all. Why then is it needed at all?

For the setupProjection() operation, two approaches seem to be in vogue: Either to use the world.nn axes selector anywhere to make sure that all textures line up, OR to use the scope.nn axes selector early in the script (typically after the first extrude of the crude building envelope), in order for all subsequent shapes to inherit the projection and thus line up.

The latter may be favorable if you're picky about e.g. brick textures to start from full bricks rather than somewhere random in the texture (provided this is how your texture is organized). With rows of individual face brick facades, this also adds more variation (at least on sloping terrains) as brick layers will not line up across individual buildings.

However, with PBR, it may vary how many material maps you may use as ao (ambient occlusion) and metalness maps may not aways be relevant, let alone provided. Nonetheless, if you're not sure which textures may ultimately be applied, I guess you still have to set up projections for all relevant texture layers…

As textures may have to be scaled differently, I prefer to add a neutral scaling of 1 to the uv axes in the setupProjection() operation and then to scale each individual texture to match in a separate texture script instead.

Texture wrapping

As texturing seems to work just fine without setting up any projections, it is all the more tedious to get the projection right, once you want to texture extruded objects. Tops and bottoms typically get diagonal stripes for textures if you do not set up a new projection. This requires a lot of fiddling around to detect the right shapes in comp() operations. Is there really no easy workaround for this?

I typically have a lot of window niches, ledges and corniches which I generate through extrude operations. Therefore, the wrapping problem emerges frequently throughout my scripts.

A related problem is texturing sloped roofs as, for some reason, textures on sloping shapes tend to also run diagonally somehow.

Mapping PBR maps to material map attributes

I am not a PBR expert, but it is not difficult for me to see that the different PBR maps have many alternating names. I am relatively sure about some synonymous maps, but others leave me hanging. An overview would be really helpful. As I understand it, CE uses 7 material shape attributes in conjunction with PBR, listed here with their corresponding texture layer numbers and their synonyms in the larger PBR world in italics as I have been able to figuring it out (correct me if I'm wrong):

0 - material.colormap - albedo, diffuse
4 - material.opacitymap - alpha
5 - material.normalmap - bump
6 - material.emissivemap
7 - material.occlusion - ambient occlusion
8 - material.roughness - gloss (opposite)
9 - material.metallicmap - metallness

Out of these, 3 maps seem to be essential – color, normal and roughness – while the remainder may only be relevant for certain types of materials.

Is my understanding correct? Are there any pitfalls which should be avoided in order not to get unexpected results?

One PBR map which is often used but seemingly doesn't map to any of the CE material shape attributes is the displacement map. This map warps the geometry beyond what can be achieved by the normal map and is thus useful for rock formations and the like. I personally have no need for that, but it might be relevant for roof tiles which are typically quite wavy. Even if displacement may not be displayed in CE, it might be handy to be able to set it up when exporting to Unreal Engine or other rendering systems. Is there a workaround for that?

By inference, the bump, dirt, and specular maps are not relevant in PBR rendering. Nonetheless, the comprehensive post From CityEngine to Unreal Engine mentions the specular material property. As I understand it, it does pretty much what the roughness property does. But are they interchangeable?

Texturing flow

In my workflow, I tend to scavenge for new textures whenever I need them and add them to a separate texture script. Therefore, organising the script in an efficient manner is of the essence. However, a lot of maps must be set up and a lot of texture layers must be projected. Therefore, setting up each texture, by my approach, amounts to some 40 lines of code:

 

attr scalingFactor = 1

# For new materials, copy Material00

# Material00	Generic material
/*
albedoMap00		= 
// opacityMap00	=
normalMap00		= 
// emissiveMap00	=
//aoMap00			= 
roughnessMap00	= 
//metalnessMap00	= 

Material00 -->
	set(scalingFactor,1)
	set(material.shader,"CityEnginePBRShader")

	set(material.colormap,albedoMap00)			# 0
	set(material.colormap.su,scalingFactor)
	set(material.colormap.sv,scalingFactor)
	
//	set(material.opacitymap,opacityMap00)		# 4

	set(material.normalmap,normalMap00)			# 5
	set(material.normalmap.su,scalingFactor)
	set(material.normalmap.sv,scalingFactor)

//	set(material.emissivemap,emissiveMap00)		# 6

//	set(material.occlusionmap,aoMap00)			# 7
	
	set(material.roughnessmap,roughnessMap00)	# 8
	set(material.roughnessmap.su,scalingFactor)
	set(material.roughnessmap.sv,scalingFactor)

//	set(material.metallicmap,metalnessMap00)	# 9

	projectUV(0)	
//	projectUV(4)	
	projectUV(5)	
//	projectUV(6)	
	projectUV(7)	
	projectUV(8)	
	projectUV(9)	
	
*/

 

By this approach, all I have to do is to copy the bracketed code, drag in the texture files for each new texture, and change the sequential map numbers in order to set up additional textures. As all maps for each texture must be equally scaled, I defined an attribute, scalingFactor, in order not to have to set identical scaling factors individually for each map.

However, someone somewhere mentioned the use of csv files (spreadsheets) as a way to rationalize the process. The approach was not documented, but I'd be happy to learn how to do that.

Best,

Nic

Tags (3)
2 Replies
CherylLau
Esri Regular Contributor

1) Projection

setupProjection() and projectUV() are necessary in order to assign UVs to the geometry.  Without UVs, textures would not be displayed.  Assets can be inserted that already have UVs assigned to them.  The primitiveX() operations (e.g. primitiveCube, primitiveSphere, etc.) insert primitives that automatically have UVs assigned.  If UVs are already exist, then setupProjection() and projectUV() are not needed.

Different UV sets exist for the different maps that are supported.  While you may wish to have a different UVs for each map (in which case you would need to set them each separately), it might also be the case that you want to use the same UVs that you use for the colormap for every other map.  In this case, you only need to make sure UV set 0 exists for the colormap, and you don't need to set UVs for the other maps because they will use UV set 0 by default if theirs doesn't exist.

https://doc.arcgis.com/en/cityengine/latest/cga/cga-texturing-essential-knowledge.htm


2) Texture wrapping

Yes, it is true that making sure the UVs are correct on all faces of a 3D model could be tedious.  As you mention, the tops and bottoms might have undesired texturing results which can happen if the UVs were setup to project onto a side face (e.g. facade).

One way to avoid that textures are smeared across the top face is to comp(f) every face separately (with the : operator, not the = operator) and apply a setupProjection in scope.xy to each face (or at least to each group of faces that are oriented the same way).  This is still a tedious process though, and it might yield disconnected textures between faces, which could also be undesirable.  At the moment, I don't think there is a better solution.

 

3) Mapping PBR maps to material map attributes

The doc has a table which maps CE material attributes to PBR terms as they are used in the GLTF material specification (see section "PBR material attributes").

https://doc.arcgis.com/en/cityengine/latest/cga/cga-material-attribute.htm


This doc page also lists which material attributes are used when the PBR shader is used (material.shader=CityEnginePBRShader).  Yes, you are correct that bumpmap, dirtmap, and specularmap are ignored when the PBR shader is used.

As for displacement maps, I wouldn't consider this a PBR material map.  Anyway, we don't have them in CE.  We only support normal maps and bump maps.

Specularity is covered by the PBR metallic and roughness properties.

 

4) Texturing flow

Yes, it is possible to store material information in a csv file and read the csv file in cga using readStringTable().

https://doc.arcgis.com/en/cityengine/latest/cga/cga-read-table-functions.htm


In CityEngine 2021.0, the setMaterial() operation can be used to set material attributes that are stored in an array.  getMaterial() also exists.  I do realize that this doesn't solve all ease-of-workflow problems though.

https://doc.arcgis.com/en/cityengine/latest/cga/cga-set-material.htm

 

0 Kudos
DevinLavigne
Occasional Contributor III

Would like to echo the need for a displacement map. While I understand CityEngine cannot support it the OP's comment about having that available downstream in another program would be very helpful.

0 Kudos