チュートリアル データは、[Help] メニュー → [Download Tutorials and Examples…] を選択し、[CityEngine Tutorial] からダウンロードできます。
Python スクリプト インターフェイスを利用すると、CityEngine の可能性をより広げることができます。このチュートリアルでは、Python Console および Editor の基本的な使用方法を解説し、CityEngine タスクの自動化の例を 2 つ紹介します。詳細については CityEngine Python reference をご参照ください。
なお、Python スクリプト インターフェイス は、すべてのバージョンの CityEngine で利用できるわけではありません。
演習 |
---|
・Part 1: Python Console および Editor |
・Part 2: 道路幅の変更 |
・Part 3: FBX ファイルからのカメラ設定 |
・Part 4: 建物の変化アニメーション |
・Part 5: アセット ライブラリ ルール ファイルの作成 |
・Part 6: startup.py を用いた CityEngine タスクの自動化 |
新規 Python Console ウィンドウを起動します。
最初の CityEngine Python コマンドとして、シーン要素を固有の名前ですばやく選択する方法を紹介します。
ce.setSelection
タイプ中に Ctrl+Space キーを押すとコマンド補完のポップアップが表示されます。
ce.setSelection(ce.getObjectsFrom(ce.scene, ce.withName("*Broadway*")))
より長く、より高度な Python コマンド、あるいはコマンド群を使おうと思ったら、すぐに CityEngine の Python Editor を使うのが便利です。
[File] メニュー → [New…] → [Python Module] から新規 Python スクリプトを作成します。Python モジュール ダイアログで、プロジェクト内の scripts フォルダーを選択します。
新しい Python モジュールに対する名前として [Name] に「myHelpers」と入力します。
[Template] ポップアップで [Module:Main] を選択します。
[OK] をクリックします。
Python モジュール myHelpers が CityEngine に組み込まれた [Python Editor] ウィンドウに開いたはずです。
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)
そしてこれをスクリプトの main 文の中で特定のパラメーターとともに使用します。
if __name__ == '__main__':
selectByAttribute("connectionStart","JUNCTION")
[Python] メニュー → [Run Script] をクリックするか、[Python Editor] ウィンドウ内で F9キーを押し、スクリプトを実行します。
別の方法として、Helper スクリプトを Python Console から呼び出すことができます。
>>> sys.path.append(ce.toFSPath("scripts"))
>>> import myHelpers
>>> myHelpers.selectByAttribute("connectionEnd", "JUNCTION")
scripting.py スクリプトを拡張するには、以下の作業を行います。
import sys
sys.path.append({PATH_TO_YOUR_SCRIPTS_DIRECTORY})
# e.g. sys.path.append("C:\user\CityEngine\MyProject\scripts")
import myHelpers
次回 CityEngine を起動した時には、myHelpers モジュールは自動的に読み込まれます。以下の方法で、Console で selection 関数を呼び出すことができます。
>>> myHelpers.selectByAttribute("connectionEnd", "JUNCTION")
任意のコードをファイル startup.py に追加することができます。Python Console が CityEngine の中で起動したときにこれが実行されます。
作成した startup.py ファイルの内容が正しく、正常に実行できることを確認してください。そうでない場合は CityEngine 内の Python コードは実行できません。startup.py を作成、変更した後に CityEngine の Python Console を開くと、スタートアップ ファイルの実行に伴う問題はそこに表示されます。
startup.py は CityEngine の起動時には読み取り専用です。変更する際は CityEngine を再起動してください。
CityEngine の起動時にスクリプトが正常に更新されない場合、Python キャッシュ ディレクトリ $USER_DIR/.cityengine/$CEVERSION_DIR/pythonCache/ を削除してください。
多くのセグメントについて道路幅の属性値を加減したいという場合は少なからずあります。GUI でこれを行うのは容易ではありませんが、簡単な Python によって実現可能です。
[Scene] フォルダーから [Tutorial_10_Python_Scripting/scenes/02_PythonScripting.cej] を開きます。
この関数は、選択されているすべての道路セグメントの streetWidth 属性値をユーザーが設定した値の分だけ増加します。
最初は関数定義です。
def incrementStreetWidth(increment):
選択されているすべてのセグメントを取得し、それらについてループします。
selectedSegments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment)
for segment in selectedSegments:
新しい道路幅を計算するために、まずコマンド ce.getAttribute() を使用して現在の値を取得する必要があります。属性名の文法上の名前には先頭に “/ce/street/” が付くことに注意してください。これによりオブジェクトのユーザー属性にアクセスすることができます。
oldWidth = ce.getAttribute(segment, "/ce/street/streetWidth")
最後に、ユーザーが与えたパラメーター increment だけ加えた新しい道路幅を計算し、新しい値をセグメントに割り当てます。
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, "streetWidth")
newWidth = oldWidth+increment
ce.setAttribute(segment, "/ce/street/streetWidth", newWidth)
スクリプトのメイン ブロックに関数呼び出しを追加し、増加量を指定します。
if __name__ == '__main__':
incrementStreetWidths(10)
複数の道路セグメントを同時選択します。
[Python] メニュー → [Run Script] をクリックするか、[Python Editor] ウィンドウ内で F9キーを押し、スクリプトを実行します。
上のスクリプトを実行するとやや時間がかかってしまいます。これはCityEngineにおいてスクリプトは別々のスレッドで実行され、GUI と [3D Viewport] ウィンドウの各コマンドが実行される度に更新されるためです。この場合は、各 setAttribute() が呼び出されるたびに道路ネットワークが更新され 3D Viewport ウィンドウ内が再描画されます。
上の例のような処理が役に立つケースでは実行は速いに越したことはありません。これは関数定義の上に @noUIupdate マーカーを追加することで実現可能です。
@noUIupdate
def incrementStreetWidth(increment):
このようにマークされた関数は実行中の GUI の更新をブロックしますが処理内容によっては数倍早く実行されます。
スクリプトコマンドによっては @noUIupdate マーカーと組み合わせることでユーザー インターフェイスがフリーズしてしまうこともありますので注意してください。 @noUIupdate を使用した時にユーザー インターフェイスがフリーズ、または予期せぬ挙動が発生した場合は、スクリプトを修正して @noUIupdate がスクリプト全体ではなく、特定の関数のみに適用されるようにします。
2 つ目の関数は同時に複数の属性 streetWidth と sidewalkWidthLeft と sidewalkWidthRight を同時に設定するものです。ユーザーは道路幅の値に掛ける数値を指定することができます。
@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)
2 つの関数
''' 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] メニュー → [Run Script] をクリックするか、[Python Editor] ウィンドウ内で F9キーを押し、スクリプトを実行します。
[Python Editor] ウィンドウ で関数の引数を設定する代わりに、[Python Console] ウィンドウでもスクリプト モジュールをインポート後に上の関数を容易に呼び出すことができます。
>> scriptpath = ce.toFSPath("scripts")
>> sys.path.append(scriptpath)
>> import setStreetWidths
>> setStreetWidths.multiplySegmentWidths(0.5)
このパートでは静的なカメラ データを Maya からの FBX へのエクスポートを経由して CityEngine に取り込む方法を紹介します。
Tutorial_10_Python_Scripting/scenes/02_PythonScripting.cej を開きます。
Maya をお持ちでない場合は、以下のステップは飛ばして予め用意してある “data/camera.fbx” ファイルを使用してください。
FBX ファイルの読み取り
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
loc = parseLine(lines, 'Property: "Lcl Translation", "Lcl Translation", "A+",')
rot = parseLine(lines, 'Property: "Lcl Rotation", "Lcl Rotation", "A+",')
return [loc,rot]
CityEngine Viewport を取得して位置と回転を設定する関数を呼び出します。
def setCamData(data):
viewport = ce.getObjectsFrom(ce.get3DViews(), ce.isViewport)[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 "+file
メイン ブロックからの呼び出し
if __name__ == '__main__':
camfile = ce.toFSPath("data/camera.fbx")
importFbxCamera(camfile)
[Python] メニュー → [Run Script] をクリックするか、[Python Editor] ウィンドウ内で F9キーを押し、スクリプトを実行します。下図のような形でカメラが配置されるはずです。
アニメーション カーブは読み込まれません。フレーム エクスポート時におけるカメラ変化 (Transformation) のみが読み込まれます。
カメラは単一のオブジェクトとしてエクスポートされる必要があります。
Python スクリプトは生成やエクスポートの処理の自動化に使用することができます。 この例では、建物の属性を設定してその結果のモデルをエクスポートすることでシンプルな建物のアニメーションの処理を 1 ステップで作成する方法を紹介します。
[Tutorial_10_Python_Scripting/scenes/03_PythonScripting.cej] を開きます。
ルール ファイルには建物のサイズを変更するための属性が用意されています。これらの値をマニュアルで設定する代わりに、値を変更して様々なモデルをバッチで生成するスクリプトを作成します。
新しい Python Main モジュール my_grow_building.py を作成します。
def growBuilding
この関数は、2 つのレンジをループする非常にシンプルなタイムラインを作成し、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
区画のオブジェクトに対し、2 つの属性の height と width が変更されます。
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
スクリプトのメイン節の中で growBuilding が呼び出されます。
if __name__ == '__main__':
growBuilding()
生成されたモデルが満足のいくものであれば、さらに 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)
これらはジオメトリ アセットおよびそれと同様のテクスチャ画像のための構造です。
def writeCGAlib():
ヘッダー情報、スタート ルール Lot および Geometries ルールを書き出します。:
cga = "/*Asset Library Loader : Generated by asset_lib.py*/\n version \"2011.1\"\n\n"
#write start rule
cga += "Lot --> Geometries Textures"
#write rule showing geometries
cga += "\n\nGeometries --> "
asset フォルダーに含まれるすべての 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,-2) 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)
最後に、メイン節の中から 2 つの関数を呼び出します。
if __name__ == '__main__':
writeCGAlib()
assignAndGenerateLib()
[Python Editor] ウィンドウ にファイル asset_lib.py を開き、F9 キーを押します。
Python を使用して、大きなタスクや反復的なタスクを自動化することができます。例えば 1 区画の情報を元に全体のモデル作成を自動化することができます。
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 CityEngine
ce.waitForUIIdle()
ce.closeFile()
if __name__ == '__main__':
fgdbToKml("data/CityData.gdb", "NewShapes", "/ESRI.lib/rules/Buildings/Building_From_Footprint.cga", "Generate")
pass
jobConfig.cfg
[config]
pathFGDB=data/CityData.gdb
layerName=NewShapes
ruleName=/ESRI.lib/rules/Buildings/Building_From_Footprint.cga
startRule=Generate
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)
この Python スクリプトの ‘startup’ セクションのコマンドは、CityEngine の起動時に自動的に実行されます。最初の起動引数は、自動化ジョブを含む CityEngine プロジェクトを定義します。これは Automation Workspace にリンクされます。2 番目の引数には設定ファイルが含まれています。これは解析され、 (name,value) ペアのリストとして自動化ジョブに引き渡されます。ジョブが完了したら、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()
Command
<Path_to_CityEngine.exe> -data <Workspace_Folder> -vmargs -DprojectFolder=<Project_Folder> -DconfigFilePath=<Configuration_FilePath>
#Example:
> "C:\Program Files\Esri\CityEngine2018.0\CityEngine.exe" -data "C:\Automation Workspace" -vmargs -DprojectFolder="C:\CE_Workspace\Tutorial_10_Python_Scripting__2018_0" -DconfigFilePath="C:\CE_Workspace\Tutorial_10_Python_Scripting__2018_0\data\jobConfig.cfg"
automationJob.py ファイルには最小限のジョブしか含まれていません。CityEngine Python ヘルプを参照して、必要に応じて調整をしてください。