教程 10:Python 脚本

要访问 CityEngine 中的教程,请单击帮助 > 下载教程和示例。 在选择教程或示例后,系统会自动下载工程并将其添加到您的工作空间。

Python 脚本接口扩展了 CityEngine 的可能性。 本教程介绍了 Python 控制台和编辑器的基本用法,并提供了多个 CityEngine 任务自动化的示例。

有关详细信息,请参阅 CityEngine Python 参考

Python 模块示例

使用 Python 控制台和编辑器

要开始此过程,请完成以下步骤:

  1. Tutorial_10_Python_Scripting 工程导入您的 CityEngine 工作空间。
  2. 打开 Tutorial_10_Python_Scripting/scenes/01_PythonScripting.cej 场景。

打开 Python 控制台

完成以下步骤以打开新的 Python 控制台:

  1. 要打开控制台窗口,单击窗口 > 控制台
  2. 单击工具栏右侧的下拉箭头以打开 Python 控制台。
    Python 控制台上的快捷菜单
  3. 您的第一个 CityEngine 命令可帮助您快速选择具有特定名称的场景元素。
  4. 输入 ce.setSelection
  5. 如果命令完成弹出窗口尚未打开,则按 Ctrl+Space 键使其显示。
  6. 输入 ce.setSelection(ce.getObjectsFrom(ce.scene, ce.withName("*Broadway*"))) 命令。
  7. Enter 键。
  8. 这将选择所有名称中包含单词 "Broadway" 的场景元素。
    Python 控制台中的命令完成(Ctrl + 空格键)
    Broadway 街道形状已选定

使用 Python 编辑器

一旦计划使用更长和更高级的 Python 命令或一组命令,在 CityEngine 中使用 Python 编辑器会很有帮助。

  1. 要创建 Python 脚本,请单击文件 > 新建 > Python 模块

    创建新 Python 模块对话框随即显示。

  2. 创建新 Python 模块对话框中,对于源文件夹,浏览至工程的脚本文件夹。
  3. 输入 myHelpers 作为新 Python 模块的名称。
  4. 单击完成

    “创建新 Python 模块”对话框

  5. 选择模块:主模板。

    “模块:主”模板

  6. 单击确定
  7. 新的 Python 模块 myHelpers 将在 CityEngine 的 Python 编辑器中打开。

在行 ce = CE() 之后添加新的 selectByAttribute(attr, value) 函数。 确保在主函数上方插入代码。

def selectByAttribute(attr, value):
    objects = ce.getObjectsFrom(ce.scene)
    selection = []
    for o in objects:
        attrvalue = ce.getAttribute(o, attr)
        if attrvalue  ==  value:
            selection.append(o)
        
    ce.setSelection(selection)

在脚本的主要子句中使用特定的参数调用它。 确保主代码块位于文件末尾。

if __name__ == '__main__':
    selectByAttribute("connectionStart","JUNCTION")

要运行脚本,请在 Python 编辑器中按 F9 键。

已选择交汇点街道形状

从控制台运行脚本

另外,您可以通过完成以下步骤来经由 Python 控制台调用帮助程序脚本:

  1. 在 Python 控制台中,将模块的路径添加到系统路径。
  2. 导入模块。

    >>> sys.path.append(ce.toFSPath("scripts"))
    >>> import myHelpers

  3. 通过以下方式在控制台中使用任意参数调用帮助程序函数:

    myHelpers.selectByAttribute("connectionEnd", "JUNCTION")

    调用自定义选择函数

扩展 scripting.py 脚本

要扩展 scripting.py,请完成以下步骤:

  1. 使用操作系统的文件浏览器在 CityEngine 工作空间中创建一个名为 scripting.py 的文件。
  2. 添加以下行以在启动时自动映射帮助程序脚本:

    import sys
     
    sys.path.append(ce.toFSPath("/Tutorial_10_Python_Scripting__2020_0/scripts"))
    import myHelpers

重新启动 CityEngine 后,将自动加载 myHelpers 模块。 您可以通过以下方式在控制台中调用选择函数:

>>> myHelpers.selectByAttribute("connectionEnd", "JUNCTION")
注:

您可以向 scripting.py 文件添加任意代码。 当打开新的 Python 控制台或从 Python 编辑器运行脚本时,脚本模块将自动运行。

确保您的 scripting.py 文件有效并且可以正确运行;否则,将无法在 CityEngine 中运行 Python 代码。 创建或修改 scripting.py 文件后,在 CityEngine 中打开 Python 控制台;运行脚本文件的问题在该处显示。

scripting.py 文件仅在 CityEngine 启动时读取一次。 如果您修改文件,请确保重新启动 CityEngine

如果在 CityEngine 启动时未正确更新脚本,请删除 Python 缓存目录 $USER_DIR/.cityengine/$CEVERSION_DIR/pythonCache/

 

更改街道宽度

通常,您可能想增加许多路段的街道宽度属性。 如果出现无法在 GUI 中快速完成此操作的情况,则 Python 脚本可以提供帮助。

打开 Tutorial_10_Python_Scripting/scenes/02_PythonScripting.cej 场景。

创建 Python 脚本

  1. 可以通过单击文件 > 新建 > Python > Python 模块创建一个规则文件。
  2. 选择工程的脚本文件夹,将其命名为 setStreetWidths,然后选择模块:主模板。

incrementStreetWidths() 函数

此函数使用用户指定的值来增加所有选定街段的 streetWidths 属性。

首先,函数定义:

def incrementStreetWidths(increment):

您需要获取所有选定的路段并将其循环。

selectedSegments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment)
segment in selectedSegments:

要计算新的街道宽度,请首先使用 ce.getAttribute() 命令获取当前值。 注意带有 /ce/street/ 前缀的属性名称的语法;这将访问对象的用户属性。

oldWidth = ce.getAttribute(segment, "/ce/street/streetWidth")

最后,通过添加用户提供的参数增量并将新值分配给路段,来计算新的街道宽度。

newWidth = oldWidth+increment
ce.setAttribute(segment, "/ce/street/streetWidth", newWidth)

整个函数如下所示:

# increment the street width parameter of all selected street segments
def incrementStreetWidths(increment):
    selectedSegments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment)
    for segment in selectedSegments:
        oldWidth = ce.getAttribute(segment, "/ce/street/streetWidth")
        newWidth = oldWidth+increment
        ce.setAttribute(segment, "/ce/street/streetWidth", newWidth)
  • 在脚本的主要块中,添加函数调用,然后选择增量。
if __name__ == '__main__':
   incrementStreetWidths(10)

选择一组街段。

通过在 Python 编辑器中按 F9 来运行 Python 脚本。

街道宽度以 10 递增
街道宽度以 10 递增

使用 @noUIupdate 标记

如果选择了很多路段,则运行之前的脚本可能需要一些时间。 这是因为 CityEngine 中的脚本在单独的线程中运行,并且在每个命令之后都会更新 GUI 和 3D 视窗。 在这种情况下,在每次调用 setAttribute() 之后,将更新街道网络,并重新绘制 3D 视窗。

尽管这对此示例很方便,但是正常的运行时间需要更快。 为此,您可以在函数定义上方添加 @noUIupdate 标记。

@noUIupdate
def incrementStreetWidths(increment):

使用这种方式标记的函数将在执行期间阻止 GUI 更新,并且根据其功能,将成倍地加快运行速度。

警告:

脚本命令与 @noUIupdate 标记的某种组合可能会冻结用户界面。

如果在使用 @noUIupdate 时遇到 UI 冻结或其他意外行为,请修改脚本,以便 @noUIupdate 仅标记一个小的特定函数,而不标记整个脚本。

multiplySegmentWidths() 函数

此函数同时设置多个属性,具体为 streetWidthsidewalkWidthLeftsidewalkWidthRight。 您可以指定一个要乘以宽度的系数。

@noUIupdate
def multiplySegmentWidths(factor):
    selectedSegments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment)
    for segment in selectedSegments:

帮助程序函数 multiplyAttribute 将对属性执行乘法运算。

      multiplyAttribute(segment, "/ce/street/streetWidth", factor)
multiplyAttribute(segment, "/ce/street/sidewalkWidthLeft", factor)
multiplyAttribute(segment, "/ce/street/sidewalkWidthRight", factor)

def multiplyAttribute(object, attrname, factor):
    oldval = ce.getAttribute(object, attrname)
    newval = oldval*factor
    ce.setAttribute(object, attrname, newval)

完成 Python 函数

# multiply street and sidewalk widths of all selected street segments by factor
@noUIupdate
def multiplySegmentWidths(factor):
    selectedSegments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment)
    for segment in selectedSegments:
        multiplyAttribute(segment, "/ce/street/streetWidth", factor)
        multiplyAttribute(segment, "/ce/street/sidewalkWidthLeft", factor)
        multiplyAttribute(segment, "/ce/street/sidewalkWidthRight", factor)

# multiply attribute of object by factor
def multiplyAttribute(object, attrname, factor):
    oldval = ce.getAttribute(object, attrname)
    newval = oldval*factor
    ce.setAttribute(object, attrname, newval)

在脚本的主要块中,添加函数调用,然后选择一个乘数。

if __name__ == '__main__':
   multiplySegmentWidths(1.5)

选择一组街段。

通过在 Python 编辑器中按 F9 来运行 Python 脚本。

所有路段宽度乘以 1.5
所有路段宽度乘以 1.5

从控制台运行

您可以在导入脚本模块后从 Python 控制台调用上述函数,而不是在 Python 编辑器中设置函数参数。

>> scriptpath = ce.toFSPath("scripts")
>> sys.path.append(scriptpath)
>> import setStreetWidths
>> setStreetWidths.multiplySegmentWidths(0.5)

 

从 FBX 文件设置照相机

接下来,您将通过从 Maya 导出的 FBX 将静态照相机数据导入 CityEngine

打开 Tutorial_10_Python_Scripting/scenes/02_PythonScripting.cej 场景。

将照相机导出到 FBX (Maya)

如果没有 Maya,则可以跳过以下步骤,并使用现有 data/camera.fbx 文件。

  1. 在 Maya 中,选择要导出的照相机。
  2. 单击文件 > 导出选择

在导出对话框中,确保按照以下屏幕截图中所示进行设置:

Maya 中照相机的 FBX 导出

照相机导入脚本

  1. 要创建规则文件,单击文件 > 新建 > Python > Python模块
  2. 选择工程的脚本文件夹,将其命名为 importFBXCamera,然后选择模块:主模板。

解析 FBX 文件

  1. 解析行并查找 ID。
  2. 准备阵列中的照相机数据。
  3. 解析存储照相机数据的 .fbx 文件中的行。

def findCameraLine(lines):
    lineId = 0
    for line in lines:
        if 'Model::camera' in line:
            return lineId
        else:
            lineId = lineId + 1
    return 0

def parseLine(lines, id):
    data = False
    for line in lines:
        if line.find(id) >=0 :
            data = line.partition(id)[2]
            break
    if data:
        data = data[:len(data)-1] # strip \n
        data = data.split(",")        
    return data

def parseFbxCam(filename):
    f=open(filename)
    lines = f.readlines()
    cnt = 0
    cameraLine = findCameraLine(lines)
    loc = parseLine(lines[cameraLine:], 'P: "Lcl Translation", "Lcl Translation", "", "A",')
    rot = parseLine(lines[cameraLine:], 'P: "Lcl Rotation", "Lcl Rotation", "", "A",')
    return [loc,rot]

def setCamData(data):
    viewport = ce.get3DViews()[0]
    setCamPosV(viewport, data[0])
    setCamRotV(viewport, data[1])

def setCamPosV(v, vec):
    v.setCameraPosition(vec[0], vec[1], vec[2])

设置 CityEngine 照相机

打开视窗窗口,然后调用位置和旋转集函数。

def setCamData(data):
    viewport = ce.get3DViews()[0]
    setCamPosV(viewport, data[0])
    setCamRotV(viewport, data[1])

def setCamPosV(v, vec):
    v.setCameraPosition(vec[0], vec[1], vec[2])
    
def setCamRotV(v, vec):
    v.setCameraRotation(vec[0], vec[1], vec[2])

主要功能

def importFbxCamera(fbxfile):
   
    data = parseFbxCam(fbxfile)
    if(data[0] and data[1]) :
        setCamData(data)
        print "Camera set to "+str(data)
    else:
        print "No camera data found in file "+fbxfile

调用主要块

if __name__ == '__main__':
    camfile = ce.toFSPath("data/camera.fbx")
    importFbxCamera(camfile)

通过在 Python 编辑器中按 F9 来运行 Python 脚本。

您的照相机应按下图中的位置放置:

运行脚本后的照相机变换

注:

不读取动画曲线;仅读取导出帧处的转换照相机。

照相机必须作为单个对象导出。

 

动画:生成建筑物

您可以使用 Python 脚本自动化生成或导出过程。 此工作流介绍了如何通过设置建筑物属性并导出结果模型集来生成建筑物动画。

打开 Tutorial_10_Python_Scripting/scenes/03_PythonScripting.cej 场景。

生成建筑物

  1. 在场景中选择地块。
  2. 如果没有生成建筑物,请单击工具栏中的生成 生成
    使用未经修改的属性生成的建筑物

规则文件包含用于更改建筑物维度的属性。 与其手动设置这些值,不如编写一个脚本来更改这些值并批量生成模型的不同版本。

动画脚本

创建一个名为 my_grow_building.py 的 Python 主模块。

def growBuilding

该函数提供了一个在两个范围内循环的时间线,并调用了 setAttribute 函数。

def growBuilding():
    for i in range(1,14):
        height = 20+i
        doStep(i,height,1)

    for i in range(15,35):
        height = 34
        width = i-14
        doStep(i,height,width)

def doStep

在地块对象上,修改了两个属性的高度和宽度。

def doStep(i,height,width):    
    object = ce.getObjectsFrom(ce.scene, ce.withName("'Lot1'"))
    ce.setAttributeSource(object, "height", "OBJECT")
    ce.setAttributeSource(object, "width", "OBJECT")
    ce.setAttribute(object, "height", height)
    ce.setAttribute(object, "width", width)
    
    Generate(object)

def Generate

以下内容可生成建筑物:

def Generate(object):
    ce.generateModels(object)

main

将在脚本的 main 子句中调用 growBuilding

if __name__ == '__main__':
   growBuilding()

批量生成建筑物

  1. 在 Python 编辑器中按 F9 以运行脚本。

建筑物序列的五个快照

批量导出

对生成的模型有信心后,添加一个名为 Export 的附加函数,并确保该函数位于主要子句之前。

def Export(i, object):
    dir = ce.toFSPath("models")
    file = "building_merge_" + str(i)
    # prepare export settings 
    settings = OBJExportModelSettings()
    settings.setBaseName(file)
    settings.setOutputPath(dir)
    # do export 
    ce.export(object, settings)

替换 doStep() 中的 Generate 调用。

#Generate(object)
Export(i, object)

您可以在 /models/ 文件夹中找到导出的模型。

 

编写资产库规则文件

如果您拥有大量资产,那么查看所有资产可能会有所帮助。 本部分将介绍如何自动生成显示工程资产的 CGA 规则文件。

打开 Tutorial_10_Python_Scripting/scenes/03_PythonScripting.cej 场景。

Python 脚本将编写具有以下结构的规则文件:

Lot -->  Geometries Textures

Geometries --> 
	 Geometry(assetpath)
	 Geometry(assetpath)
	 ...

Geometry(asset) --> i(asset)

这用于几何资产和纹理图像。

  • 创建 Python 主模块 asset_lib.py
  • 添加新函数 writeCGALib
def writeCGAlib():

编写标题信息、起始规则 LotGeometries 规则。

cga = "/*Asset Library Loader : Generated by asset_lib.py*/\n version \"2020.0\"\n\n"

# write start rule
cga += "Lot -->  Geometries Textures"

# write rule showing geometries
cga += "\n\nGeometries --> "

迭代资产文件夹中的所有 .obj 文件,并为每个资产准备规则调用 Geometry(assetpath)

# get all .obj files from asset directory, and call their loader
for obj in ce.getObjectsFrom("/", ce.isFile, ce.withName("/Tutorial_10*/assets/*.obj")):   
# and write
cga += "\n\t t(2,0,0)  Geometry(\""+obj+"\")"

为纹理资产编写类似的规则。

# write rule showing jpg textures
cga+="\n\nTextures -->
\n\ts(1,0,0) set(scope.ty,3) set(scope.tz,0) i(\"facades/xy-plane.obj\")"   

# get all .jpg files from asset directory, and call their loader
for jpg in ce.getObjectsFrom("/", ce.isFile, ce.withName("/Tutorial_10*/assets/*.jpg")):
    cga += "\n\tt(2,0,0)  Texture(\""+jpg+"\")"

编写资产加载程序规则。

# write geometry loader rule
cga += "\n\n Geometry(asset) --> 
s(1,0,0) i(asset) set(scope.ty,0) set(scope.tz,0)"

# write texture loader rule
cga += "\n\n Texture(asset) --> set(material.colormap, asset)"

打开 .cga 文件的文件句柄,然后编写 CGA 内容。

cgafile = ce.toFSPath("rules/asset_lib.cga")
CGA = open(cgafile, "w")
CGA.write(cga)
CGA.close()
print "written file "+cgafile

添加新的 assignAndGenerateLib() 函数。 它将生成的 .cga 文件分配给场景地块并生成模型。

def assignAndGenerateLib():
    object = ce.getObjectsFrom(ce.scene, ce.withName("'Lot2'"))
    ce.refreshWorkspace()
    ce.setRuleFile(object, "asset_lib.cga")
    ce.setStartRule(object, "Lot")
    ce.generateModels(object)

最后,在主要子句中调用两个函数:

if __name__ == '__main__':
    writeCGAlib() 
    assignAndGenerateLib()

生成库模型

在 Python 编辑器中,打开 asset_lib.py 文件,然后按 F9

资产库

 

使用 startup.py 自动执行 CityEngine 任务

您可以使用 Python 自动执行大型任务或重复性任务。 例如,您可能想根据整个县的宗地信息自动生成模型。

要自动执行任务,请执行以下操作:

  1. 创建一个工程或使用现有的工程。 对于本例,请使用现有的 Tutorial_10_Python_Scripting__2020_0 教程工程。
  2. 在工程的脚本文件夹中创建一个 Python 主模块。 该教程包含一个名为 automationJob.py 的基本作业。
  3. 插入自动化作业的带有任务的函数。 为了进行测试,请在 '__main__' 部分中添加对此函数的调用。 所提供的 fgdbToKml 示例从 fileGDB 导入形状以生成模型并将其写出到 KML:
    automationJob.py
    from scripting import *
    # get a CityEngine instance
    ce = CE()
     
    def fgdbToKml(pathFGDB,layerName,ruleName,startRule = "Generate"):
        # open scene in the automation project
        ce.newFile('/scenes/emptyScene.cej')
         
        # load a database
        importSettings = FGDBImportSettings()
        importSettings.setDatasetFilter(['/'+layerName])
        ce.importFile(ce.toFSPath(pathFGDB), importSettings)
         
        # assign rule file based on the layer name
        layer = ce.getObjectsFrom(ce.scene, ce.isShapeLayer, ce.withName(layerName))[0]
        shapes = ce.getObjectsFrom(layer, ce.isShape)
        ce.setRuleFile(shapes, ruleName)
        ce.setStartRule(shapes, startRule)
         
        # export models to KML
        exportSettings = KMLExportModelSettings()
        exportSettings.setOutputPath(ce.toFSPath("models"))
        exportSettings.setBaseName(layerName)
        exportSettings.setCompression(True)
        ce.export(shapes, exportSettings)
         
        # close Scene
        ce.waitForUIIdle()
        ce.closeFile()
    
    if __name__ == '__main__':
        fgdbToKml("data/CityData.gdb", "NewShapes", 
                  "/ESRI.lib/rules/Buildings/Building_From_Footprint.cga", "Generate")
        pass
  4. 创建一个配置文件以定义作业参数。 该教程包含位于 \data\jobConfig.cfg 中的示例。
    jobConfig.cfg
    [config]
    pathFGDB=data/CityData.gdb
    layerName=NewShapes
    ruleName=/ESRI.lib/rules/Buildings/Building_From_Footprint.cga
    startRule=Generate
  5. 将函数 run(cfg)getCfgValue(cfg,name) 添加到 automationJob.py 以便使用存储在配置文件中的参数来运行自动化作业。
    automationJob.py
    ...
    def getCfgValue(cfg,name):
        for c in cfg:
            if  c[0] == name: return c[1]
        return None
     
    def run(cfg):
        pathFGDB = getCfgValue(cfg,'pathfgdb')
        layerName = getCfgValue(cfg,'layername')
        ruleName = getCfgValue(cfg,'rulename')
        startRule = getCfgValue(cfg,'startrule')
         
        fgdbToKml(pathFGDB, layerName, ruleName, startRule)
  6. 建议使用单独的 CityEngine 工作空间进行自动化。 在您的系统上创建一个名为 C:\Automation Workspace 的文件夹。
  7. /scripts/startup.py 文件从教程工程复制到新的 C:\Automation Workspace 根目录。

    启动 CityEngine 时会自动运行此 Python 脚本的 '__startup__' 部分中的命令。 第一个启动参数定义包含自动化作业的 CityEngine 工程。 它将链接到自动化工作空间。 第二个参数包含 config 文件。 它将被解析并以(名称,值)对列表的形式移交给自动化作业。 作业完成后,CityEngine 将安全地关闭。

    startup.py
    from scripting import *
    from java import lang
    import ConfigParser, sys
     
    if __name__ == '__startup__':
        # get a CityEngine instance
        ce = CE()
         
        # get startup arguments
        projectFolder = lang.System.getProperty("projectFolder")
        configFilePath = lang.System.getProperty("configFilePath")
         
        # link the automation project into automation workspace
        if "automationProject" in ce.listProjects(): ce.removeProject("automationProject")
        ce.importProject(projectFolder, False, "automationProject")
         
        # read configuration file
        cp = ConfigParser.ConfigParser()
        cp.read(configFilePath)
        cfg = cp.items('config') # list of (name,value) pairs
         
        # run automation job
        sys.path.append(ce.toFSPath("/automationProject/scripts"))
        import automationJob
        automationJob.run(cfg)
         
        # safely shut down CityEngine
        ce.exit()
  8. 打开命令行并在自动化工作空间中启动 CityEngine,然后移交作业定义和参数:
    命令
    <Path_to_CityEngine.exe> -data <Workspace_Folder> -vmargs 
    -DprojectFolder=<Project_Folder> -DconfigFilePath=<Configuration_FilePath>
     
    @ Example:
    > "C:\Program Files\Esri\CityEngine2020.0\CityEngine.exe" -data 
    "C:\Automation Workspace" -vmargs 
    -DprojectFolder="C:\CE_Workspace\Tutorial_10_Python_Scripting__2020_0" 
    -DconfigFilePath="C:\CE_Workspace\Tutorial_10_Python_Scripting__2020_0\data\jobConfig.cfg"
  9. 作业完成后,\models 文件夹包含 Footprints.kmz 输出文件,该文件显示在 ArcGIS Earth 的下方。

    ArcGIS Earth 中的结果

注:
automationJob.py 文件包含一个基本作业。 使用 CityEngine Python 参考对其进行调整以适应您的需求。