Unreal Engine 5とpythonで星空を作る
blenderで星空を作ったものの、UE5で使用するには使い勝手が悪かったのでUnreal Pythonを使用してUE5で星空を作ってキューブマップにした話
1.目標
- 9000個の星を生成
- CubeMapテクスチャ作成
- 天球用Sphereのマテリアル作成
2.結果
#UE5
— 縦長おじさん (@tatenagaOG3) 2022年11月6日
Unreal Pythonで9000個位のSphereを使った星空を作った。早くて軽くて綺麗・・・ pic.twitter.com/0jRJKOUTI1
3.pythonコード
とりあえず動くレベルの汚ソースです
# BSC5を使用してUE5に星を配置するスクリプト import unreal import math CATALOG_PATH = "D:\\BlenderProjects\\starcatalog\\bsc5.dat" # 見かけの等級下限 6.xxまでの星を収録している。0に近づけるほど表示は少なくなる MAX_VMAG = 7.0 # 天球の半径 18kmに設定してみた CELESTIA_RADIUS = 18000* 100 # 0等級の基準となる星のscale BASE_STAR_SCALE = 100 def get_right_ascension(hour:int, min:int, sec:float): """h,m,sから赤経をradianで返す :param 1hour = 15 :param 1min = 15/60 :param 1sec = 15/3600 :return RA radians """ ra_deg = hour * 15 + (min * 15 / 60) + (sec * 15 / 3600) return math.radians(ra_deg) def get_declination(deg:int, arcmin:int, arcsec:int): """deg,arcmin,arcsecから赤緯をradianで返す :param 1deg = 1 :param 1arcmin = 1/60 :param 1arcsec = 1/3600 :return DEC radians """ dec_deg = deg + (arcmin * 1 / 60) + (arcsec * 1 / 3600) return math.radians(dec_deg) def get_visual_radius(vmag:float): """vmagの値から0等級の球に対するscale比率を返す :param vmag vmag :return scale_ratio 基準とする球に対するスケールを返す """ if (vmag > 0): scale = BASE_STAR_SCALE / math.sqrt(math.pow(10, 2/5 * vmag)) elif(vmag <= 0): scale = BASE_STAR_SCALE * math.sqrt(math.pow(10, 2/5 * (0 - vmag))) elif(vmag == 0): scale = BASE_STAR_SCALE return scale def create_SphereActor(label:str, locationX:float, locationY:float, locationZ:float, scale:float): """Sphereを生成・配置 :label outlinerに表示される名前 :location XYZ root_componentの座標(cm) :scale Sphereの大きさ (直径100cm * scale) """ # EALを使用してベーシックの球体を選択 EAL = unreal.EditorAssetLibrary() asset = EAL.find_asset_data('/Engine/BasicShapes/Sphere.Sphere') sphereObj = asset.get_asset() # EASで新しいアクターをワールドに配置 EAS = unreal.EditorActorSubsystem() locations = unreal.Vector(locationX, locationY, locationZ) newActor = EAS.spawn_actor_from_object(object_to_use=sphereObj, location=locations, transient=False) # 配置したアクター名の変更 newActor.set_actor_label(label) # 配置したアクターのサイズ変更 rootComp = newActor.get_editor_property("root_component") newScale = unreal.Vector(scale, scale, scale) rootComp.set_editor_property("relative_scale3d", newScale) return newActor def set_New_Material(actor:unreal.Actor, specTpye:str): """SMアクターのマテリアルをMK分類に基づいたマテリアルに変更する :actor create_SphereActorで作成したアクター :specType MK分類 [O, B, A, F, G, K, M]のいずれかを指定 """ EAL = unreal.EditorAssetLibrary() assetpath = '' if(specTpye == 'O'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_O_Type_000.MI_O_Type_000' elif(specTpye == 'B'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_B_Type_000.MI_B_Type_000' elif(specTpye == 'A'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_A_Type_000.MI_A_Type_000' elif(specTpye == 'F'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_F_Type_000.MI_F_Type_000' elif(specTpye == 'G'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_G_Type_000.MI_G_Type_000' elif(specTpye == 'K'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_K_Type_000.MI_K_Type_000' elif(specTpye == 'M'): assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_M_Type_000.MI_M_Type_000' else: assetpath = '/Game/Developers/raku5/Collections/Blueprints/Materials/MI_O_Type_000.MI_O_Type_000' newMaterial = EAL.find_asset_data(assetpath).get_asset() actor.get_editor_property("static_mesh_component").set_material(0, newMaterial) def main(): with open(CATALOG_PATH, 'r', encoding='UTF-8') as rf: for line in rf: try: vmag = float(line[102:107]) except: continue if (vmag > MAX_VMAG): continue # 配置に必要な情報をスライス hr = int(line[0:4]) ra_hours = int(line[75:77]) ra_minutes = int(line[77:79]) ra_seconds = float(line[79:83]) dec_sign = line[83] dec_degrees = int(line[84:86]) dec_arcmin = int(line[86:88]) dec_arcsec = int(line[88:90]) spectral_type = line[129] # 配置に必要な情報に加工 star_name = f'HR.{hr}' ra_rad = get_right_ascension(ra_hours, ra_minutes, ra_seconds) dec_rad = get_declination(dec_degrees, dec_arcmin, dec_arcsec) star_location_x = CELESTIA_RADIUS * math.cos(dec_rad) * math.cos(ra_rad) # UEの座標系による -1 star_location_y = CELESTIA_RADIUS * math.cos(dec_rad) * math.sin(ra_rad) * -1.0 star_location_z = CELESTIA_RADIUS * math.sin(dec_rad) if (dec_sign == '-'): star_location_z *= -1.0 star_scale = get_visual_radius(vmag) newStar = create_SphereActor(star_name, star_location_x, star_location_y, star_location_z, star_scale) set_New_Material(newStar, spectral_type) print('Create Success')
4.メモ
blenderで星空を作った時のコードを流用。Unreal Engineで使う際に変更した点
- Y軸の方向が違うので修正
- Sphereの大きさがscaleで表現されているので、それに合わせた修正
マテリアルの作成
unlit
な星用マテリアルを作成してMK分類7種分のマテリアルインスタンスを作成。調整用にパラメータを用意した。
Cubemapの作成
Cube Render Target
を作成してワールドに配置すると周囲をキャプチャしてくれる。
Mip Gen Settings
をNoMipmapsに、Size X
初期値512では解像度が足りなかったので最終的には2048に設定した。
Create Static Texture
でテクスチャ化できる
skydome用マテリアルの作成
[ue4 skydome]で検索してヒットした動画を参考にして、用意したCubemapを使用、地軸の傾き用パラメータと回転用のパラメータを作成した。
5.感想
blenderでは見た目等級を5にして星が2000個を超えたあたりで、処理時間を待てず離席して休憩するレベルだったが、UE5は見た目等級を最大にして約9000個の星を生成しても処理時間は1分も掛からなかったのには驚き。 ポストプロセスで星をキラキラさせたりも軽かった。
星空作るのに約1週間blender と unreal engine のpythonで遊んだが、何かオリジナリティーのある事したいなぁ・・・面倒な作業といえばRig周り作業、リターゲティングとかもpythonでサクッとできたらいいことあるかも?
Unreal Pythonを使ったオブジェクトの配置とマテリアルの設定まで
UEでpython使ってみた。という記事はヒットするけれど、その先のちょっとした使い方までとなると検索力不足のためか中々見つからなかった。『UE5で星空を作る』の前段階としてUnreal Pythonの始め方からオブジェクトの配置・マテリアルの設定までを記録しておく
0. Unreal Pythonの始め方
ハローワールド
Output Log
のドロップダウンメニューからPython
を選択すると使える
ファイルを実行する準備
pythonスクリプトを作成するディレクトリを適当な場所に作成してAdditional Paths
へパスを追加する。この時Developer Mode
にチェックを入れてstubファイルを生成するとTextエディタのオートコンプリートに使用できる
VScodeの設定
VScodeの設定でpythonオートコンプリートにUE5が生成したstubのpathを指定する
ファイルのimportとreload
import [ファイル名(py抜き)]
でインポートとスクリプトの実行がされる。ファイルを編集を反映させるにはimportlib
のreload
が必要になる
1. 目標
- コンテンツブラウザからアセットを選択
- 選択したアクターをワールドに配置
- 配置したアクターのマテリアルを設定
2. 結果
3.ソース
import unreal def get_Basic_ShapeData(shape:str) -> unreal.AssetData: """ベーシックシェイプの名前からAssetDataを返す shape : 'Cube', 'Sphere', 'Plane' .. """ EAL = unreal.EditorAssetLibrary shapeList = EAL.list_assets('/Engine//BasicShapes') for shapePath in shapeList: if(shape in shapePath): shapeData = EAL.find_asset_data(shapePath) return shapeData def create_StaticMeshActor_from_BasicShapeData(assetData:unreal.AssetData, x:float, y:float, z:float) -> unreal.StaticMeshActor: """StaticMeshActorをワールドにスポーン、生成したアクターを返す assetData: ベーシックシェイプのアセットデータ return: 生成したアクター """ EAS = unreal.EditorActorSubsystem() return EAS.spawn_actor_from_object(assetData.get_asset(), location=[x, y, z]) def set_New_Material(actor:unreal.Actor, materialName:str): """アクターに新しいマテリアルをセットする actor: ワールド内に配置されているアクター materialName: Content下に保存されているアセット名 """ materialObj = None EAL = unreal.EditorAssetLibrary assetList = EAL.list_assets('/Game') for path in assetList: if(materialName in path): materialObj = EAL.find_asset_data(path).get_asset() smc = get_Static_Mesh_Component(actor) smc.set_material(0, materialObj) def get_Static_Mesh_Component(actor:unreal.StaticMeshActor) -> unreal.StaticMeshComponent: """インテリセンスが効かないので get_editor_propertyをラップした関数 """ return actor.get_editor_property("static_mesh_component") def set_position(actor:unreal.StaticMeshActor, x:float, y:float, z:float): newlocation = unreal.Vector(x, y, z) actor.set_actor_location(newlocation, False, False) def resize_actor(actor:unreal.StaticMeshActor, size:float): newSize = unreal.Vector(size,size,size) actor.set_actor_scale3d(newSize) def rename_actor(actor:unreal.StaticMeshActor, label:str): actor.set_actor_label(label) # サンプル用の処理 def test(): shape = get_Basic_ShapeData('Cone') newActor = create_StaticMeshActor_from_BasicShapeData(shape, 0, 0, 100) set_New_Material(newActor, 'M_Ground_Moss') rename_actor(newActor,'HappyCone') resize_actor(newActor, 2.5)
4. ソースのメモ
基本は以下の2つのクラスを使用していく
EditorAssetLibrary
: コンテンツブラウザを操作するEditorActorSubsystem
:アウトライナーを操作する感じ?
アセットpathのリストの取得
EAL = unreal.EditorAssetLibrary shapeList = EAL.list_assets('/Engine//BasicShapes')
たまたまベーシックシェイプの場所はEngineディレクトリ下だったので問題はなかったが、Content
ディレクトリの場合は/Game
とする必要がある
インテリセンスが効いたり効かなかったり
smc = get_Static_Mesh_Component(actor) smc.set_material(0, materialObj) def get_Static_Mesh_Component(actor:unreal.StaticMeshActor) -> unreal.StaticMeshComponent: return actor.get_editor_property("static_mesh_component")
もともとこの部分はactor.get_editor_property("static_mesh_component").set_material(0, materialObj)
とまとめて書いてたが、プロパティ部分の操作でインテリセンスが効かないのが不安なので戻り値のクラスを指定する関数でラップした。
5. 感想
記事にするまでpythonの戻り値の型指定を知らなかったレベルの素人がいうのもなんだけどプロパティをドキュメントで調べないと指定できなかったりクラスを継承しまくりで使いづらいなぁという印象。使いこなすうちに慣れるはず・・・
BlenderでBSC5を使って星空を作る
オリオン座くらいしか知らない程度の人がBright Star Catalogを使って天球上に星を配置した話
1.目標
2.結果
3.pythonコード
# bright star catalog を使用してblenderに星をマッピングするスクリプト # とりあえず天球の軸をz,春分点をx軸とする from http.client import TEMPORARY_REDIRECT import bpy from cmath import sin import math CATALOG_PATH = "D:\\BlenderProjects\\starcatalog\\bsc5.dat" # 見かけの等級下限 6.xxまでの星を収録している。0に近づけるほど表示は少なくなる MAX_VMAG = 5.5 # 天球の半径 地平線までの距離4kmを設定してみた CELESTIA_RADIUS = 4000 # 0等級の星を表す半径 ZERO_MAG_RADIUS = 2.0 # MK分類の温度初期設定 単位K ハーバード分類の有効温度最小値を参考 O_TYPE_TEMP = 30000.0 B_TYPE_TEMP = 10000.0 A_TYPE_TEMP = 7500.0 F_TYPE_TEMP = 6000.0 G_TYPE_TEMP = 5200.0 K_TYPE_TEMP = 3700.0 M_TYPE_TEMP = 2400.0 def get_right_ascension(hour:int, min:int, sec:float): """h,m,sから赤経をradianで返す :param 1hour = 15 :param 1min = 15/60 :param 1sec = 15/3600 :return RA radians """ ra_deg = hour * 15 + (min * 15 / 60) + (sec * 15 / 3600) return math.radians(ra_deg) def get_declination(deg:int, arcmin:int, arcsec:int): """deg,arcmin,arcsecから赤緯をradianで返す :param 1deg = 1 :param 1arcmin = 1/60 :param 1arcsec = 1/3600 :return DEC radians """ dec_deg = deg + (arcmin * 1 / 60) + (arcsec * 1 / 3600) return math.radians(dec_deg) def get_visual_radius(vmag:float): """vmagの値から球の半径を返す :param vmag vmag :return rad bpyで作成するスフィアの半径 """ if (vmag > 0): ratio = math.pow(10, 2/5 * vmag) rad = ZERO_MAG_RADIUS / math.sqrt(ratio) elif(vmag <= 0): ratio = math.pow(10, 2/5 * (0 - vmag)) rad = ZERO_MAG_RADIUS * math.sqrt(ratio) return rad def remove_all_materials(): """実行前にblender上からマテリアルを一括削除する関数 """ material_collection = bpy.context.blend_data.materials if(len(material_collection) > 0): for i in material_collection: material_collection.remove(i) def make_materials(): """MK分類で7種のマテリアルを作成して返す """ material_collection = bpy.context.blend_data.materials o_type = material_collection.new('M_o_type.001') b_type = material_collection.new('M_b_type.001') a_type = material_collection.new('M_a_type.001') f_type = material_collection.new('M_f_type.001') g_type = material_collection.new('M_g_type.001') k_type = material_collection.new('M_k_type.001') m_type = material_collection.new('M_m_type.001') # Blackbody->Emission->Material Output のノード、リンクを作成 for i in material_collection: i.use_nodes = True i.node_tree.nodes.clear() # emission->material output mat_out = i.node_tree.nodes.new(type='ShaderNodeOutputMaterial') emit = i.node_tree.nodes.new(type='ShaderNodeEmission') mat_out_input = mat_out.inputs['Surface'] emit_output = emit.outputs['Emission'] i.node_tree.links.new(mat_out_input, emit_output) # blackbody->emission bl_body = i.node_tree.nodes.new(type='ShaderNodeBlackbody') bl_body_output = bl_body.outputs['Color'] emit_input = emit.inputs['Color'] i.node_tree.links.new(emit_input, bl_body_output) # blackbody temper bl_body_temper = bl_body.inputs[0] if(i.name == o_type.name): bl_body_temper.default_value = O_TYPE_TEMP elif(i.name == b_type.name): bl_body_temper.default_value = B_TYPE_TEMP elif(i.name == a_type.name): bl_body_temper.default_value = A_TYPE_TEMP elif(i.name == f_type.name): bl_body_temper.default_value = F_TYPE_TEMP elif(i.name == g_type.name): bl_body_temper.default_value = G_TYPE_TEMP elif(i.name == k_type.name): bl_body_temper.default_value = K_TYPE_TEMP elif(i.name == m_type.name): bl_body_temper.default_value = M_TYPE_TEMP return material_collection # 実際の処理 remove_all_materials() mat_collection = make_materials() with open(CATALOG_PATH, 'r', encoding='UTF-8') as rf: for line in rf: # 恒星以外またはMAX_VMAGより暗い星は除外 try: vmag = float(line[102:107]) except: continue if (vmag > MAX_VMAG): continue # 配置に必要な情報をスライス hr = int(line[0:4]) ra_hours = int(line[75:77]) ra_minutes = int(line[77:79]) ra_seconds = float(line[79:83]) dec_sign = line[83] dec_degrees = int(line[84:86]) dec_arcmin = int(line[86:88]) dec_arcsec = int(line[88:90]) # 配置に必要な情報に加工 ra_rad = get_right_ascension(ra_hours, ra_minutes, ra_seconds) dec_rad = get_declination(dec_degrees, dec_arcmin, dec_arcsec) location_x = CELESTIA_RADIUS * math.cos(dec_rad) * math.cos(ra_rad) location_y = CELESTIA_RADIUS * math.cos(dec_rad) * math.sin(ra_rad) location_z = CELESTIA_RADIUS * math.sin(dec_rad) if (dec_sign == '-'): location_z *= -1.0 star_radius = get_visual_radius(vmag) # 配置 bpy.ops.mesh.primitive_uv_sphere_add(segments=8, ring_count=12, radius=star_radius, enter_editmode=False, align='WORLD', location=(location_x, location_y, location_z), scale=(1, 1, 1)) target = bpy.context.object target.name = f'HR.{hr}' bpy.ops.object.shade_smooth() # MK分類のアルファベットのみを参照する spectral_type = line[129] if(spectral_type == 'O'): target.data.materials.append(mat_collection[0]) elif(spectral_type == 'B'): target.data.materials.append(mat_collection[1]) elif(spectral_type == 'A'): target.data.materials.append(mat_collection[2]) elif(spectral_type == 'F'): target.data.materials.append(mat_collection[3]) elif(spectral_type == 'G'): target.data.materials.append(mat_collection[4]) elif(spectral_type == 'K'): target.data.materials.append(mat_collection[5]) elif(spectral_type == 'M'): target.data.materials.append(mat_collection[6]) hr = int(line[0:4]) print(f'HR: {hr} VMAG: {vmag} Spectral: {spectral_type} RA: {ra_rad} DEC: {dec_sign}{dec_rad} x:{location_x} y:{location_y} z:{location_z} size: {star_radius}')
4. 解説(振り返り)
突如として「星空が作りたいなぁ」思いついてからとりあえず完成まで3日ほどかかった。しばらくすると忘れてしまうだろうからせっかくなので記録しておく
4-1. 元データの入手
[天体 座標]などで検索してWikiペディアの読んでいるうちに星表を発見し、その記事から輝星星表の存在を知る。外部リンクからYale Bright Star Catalogのページへアクセスしてgzip形式に圧縮されていたBSC5.dat
を入手した。
同ページに公開されているReadMeもdatファイル中の内容を理解するのに不可欠。
4-2. blender python でMaterialを操作する
blender pythonを使用するときにヒントとしてEditor type
をinfor
にして作業したlogから推測できるが、アウトライナー周辺は例外で面倒だった。
既存のMaterialを削除する
Blender FileのMaterialsを削除する。マウス操作ではアウトライナー上でドロップダウンメニューから選択して削除可能だが、infoは外れでほぼノーヒントbpy.context.blend_data
を探すことになった。
material_collection = bpy.context.blend_data.materials if(len(material_collection) > 0): for i in material_collection: material_collection.remove(i)
新規Materialを作成する
bpy.context.blend_data.materials.new([name])
で作成できる。星のマテリアルとして、shaderをEmitter、colorをBlackbodyといったシンプルなものを作成した。
- nodeを有効
- 初期ノードを一旦clear (デフォルトでPrincipled BSDFがあるので)
- Material Output, Emissionを作成してピンを接続
- BlackBodyを作成してEmissionとピンを接続
- BlackBodyのtempパラメータを設定
# 1 i.use_nodes = True # 2 i.node_tree.nodes.clear() # 3 # emission->material output mat_out = i.node_tree.nodes.new(type='ShaderNodeOutputMaterial') emit = i.node_tree.nodes.new(type='ShaderNodeEmission') mat_out_input = mat_out.inputs['Surface'] emit_output = emit.outputs['Emission'] i.node_tree.links.new(mat_out_input, emit_output) # 4 # blackbody->emission bl_body = i.node_tree.nodes.new(type='ShaderNodeBlackbody') bl_body_output = bl_body.outputs['Color'] emit_input = emit.inputs['Color'] i.node_tree.links.new(emit_input, bl_body_output) # 5 # blackbody temper bl_body_temper = bl_body.inputs[0] if(i.name == o_type.name): bl_body_temper.default_value = O_TYPE_TEMP
プリミティブのオブジェクトを追加
ワールドに星を配置していく作業。特段難しい部分はなかったが、シェーディングをsmoothにするコマンドがbpy.ops.object.shade_smooth
で、コンテキストを使用しない事に疑問を感じた。
bpy.ops.mesh.primitive_uv_sphere_add(segments=8, ring_count=12, radius=star_radius, enter_editmode=False, align='WORLD', location=(location_x, location_y, location_z), scale=(1, 1, 1)) target = bpy.context.object target.name = f'HR.{hr}' bpy.ops.object.shade_smooth() # MK分類のアルファベットのみを参照する spectral_type = line[129] if(spectral_type == 'O'): target.data.materials.append(mat_collection[0])
5. 感想
blender pythonを使った面倒な作業第2弾『星空の作成』は忘れかけのpythonと三角関数を思い出させてくれたり、完成したものはちょっと綺麗でやってよかったと満足度は高かったが・・・
肝心の目的の作成したパノラマ画像をUE5でskydomeのテクスチャに使用するというのがうまくできなかった。(要Cubemap)のは残念。blender上でcubemapを作成するのも面倒なので結局Unreal Pythonで星空を作った。
第18回UE5ぷちコンを終えた感想
まずは反省点
- GameModeやGameInstanceなどの用意されている機能の存在と使い方を知らなかった
- Level BlueprintにタイマーやUIの生成を割り当ててアクセスしづらくて苦労した
- Developersフォルダを使わずプロジェクト内にテスト用のサンプルが入り混じって目も当てられない
- Blueprintのコメントグループは使用したがdescriptionを使用しなかったので自分でみてもよくわからない
- ついUE5で遊びすぎてぷちスタ、ぷちコン終了後に謎の疲労感に数日悩まされた
次に向けて
また参加できる機会があるならこうしようかと思ってること
ぷちスタではタスクにそった最小のプロジェクトを無理なく作成する
ぷちコンではぷちスタで作成した最小のプロジェクトに少し手を加えて発展させる
抽象的なぷちスタの状態から具体的な形にする(タイマーをただの数字の表示から「プレイヤーを潰そうと下がってくる天井」や「導火線に火が付いた何か」などのストーリー性を持たせたり)プロジェクトをいつでも提出できる状態に保つ
日ごろからアセットの作成や収集をしておく
感想
まずは何でもいいから作ってみるという機会に挑戦できてよかった。UE5を触り始めて最初の1か月に抱いていた「情報量が少ない」という認識も終わってみればわからない事が何なのかわからない状態だったと気づけた。触っているうちに目にする機能やキーワードで調べるとだいたい先駆者が記事を書いているので問題は解決できると思う。
そんなわけでしばらくは何でもいいから作ってみるを繰り返し、最終的には美麗グラフィックな縦長のゲームを作りたい。
Font MaterialでUMGのテキストを派手にした
今回はゲーム中の画面に表示されるテキストの見栄えを良くしたいなということで、UMGのTextへFont Materialを適用した話をメモする。そもそもUMGって何の略なの?という疑問が生じたので調べたらUnreal Motion Graphicsの事らしい。
出来たもの
メモ:UE5のText WidgetへFont Materialを追加したらこうなった。 pic.twitter.com/mG00Alhob4
— 縦長おじさん (@tatenagaOG3) August 30, 2022
手順
1. マテリアルを作成してUI用にする
- MaterialのDetailsからMaterial Domain
をUser Interface
に変更
- Blend Mode
をTranslucent
に変更
- TexCoordノードとPannerを使用してUVを動かす
- PannerのDetailsにある
Speed X
,Speed Y
へ直接入力か、Speedへパラメータノードを接続する
- PannerのDetailsにある
- 傾きを付ける
- CustomRotatorノードを追加
- Rotation Angleへ定数ノードを接続
- Texture Sampleで動かしたいテクスチャを設置
- 見た目の加工
- Subtractノードで反転
- vector3で好みの色と足した
まとめ
なんとなくそれっぽいのができたが、どうしてそうなっているのかあまりよく理解していない。Pannerが優秀すぎるのが新しい発見だった。
- Blend Mode
- Maskの仕方
- Add, Multiple, Subtractなどの挙動
とか暇なときに調べておきたい
BlenderへPLATEAUのモデルを2時間40分かけてインポートした話
昨日は、はじめてBlender pythonを使用してimportした4~5000個ほどのオブジェクトを7つのオブジェクトにまとめ、マテリアルのセット、UV unwrapといった作業を自動化した。
いつかfbxのimportも自動化したいな思っていたが暇だったのでやってみた。
1.ソース
pythonの作法とか知らないので自分で分かればいいやレベルの汚ソース。target_dir
のFPXAssets
は綴りを間違えているがあえて放置している。
import glob import bpy import math target_dir="D:\\FPXAssets\\13100_tokyo23-ku_2020_fbx_3_op\\bldg\\lod1" search_key="533925" path_list=glob.glob(target_dir + '\\' + search_key + '*') # コレクションの検索用関数 def recurLayerCollection(layerColl, collName): found = None if (layerColl.name == collName): return layerColl for layer in layerColl.children: found = recurLayerCollection(layer, collName) if found: return found # fbxデータ内のオブジェクトをランダムで7個にする関数 def randomJoinObjects(): divNum = [0.14, 0.17, 0.2, 0.25, 0.33, 0.5, 1] sd = 1 for div in divNum: bpy.ops.object.select_random(ratio=div, seed=sd) # Join先のオブジェクトを有効化 target = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = target bpy.ops.object.join() # UV unwrap処理 if(target.data.uv_layers=='None'): bpy.ops.mesh.uv_texture_add() bpy.ops.object.editmode_toggle() bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=0, area_weight=0, correct_aspect=True, scale_to_bounds=False) bpy.ops.object.editmode_toggle() # マテリアルのセット if target.data.materials: target.data.materials[0] = bpy.data.materials[sd] else: target.data.materials.append(bpy.data.materials[sd]) # hideしてワンセット終わり for o in bpy.context.selected_objects: o.hide_set(True) # ランダムシード値の更新 sd += 1 for f in path_list: # 新しいコレクションの名前を作成 i=f.find(search_key) new_col_name=f[(i+len(search_key)):(i+len(search_key)+2)] # コレクションを作成 new_col = bpy.data.collections.new(new_col_name) bpy.context.scene.collection.children.link(new_col) # 作成したコレクションのアクティブ化 layer_collection=bpy.context.view_layer.layer_collection layer_col = recurLayerCollection(layer_collection, new_col.name) bpy.context.view_layer.active_layer_collection = layer_col # FBXファイルのインポート bpy.ops.import_scene.fbx(filepath=f) # ランダムに7つのオブジェクトへマージする処理 # import後はすべて選択されているので選択を解除しておく bpy.ops.object.select_all(action='DESELECT') randomJoinObjects()
2. ソースのメモ
モデルの加工部分は前の記事で書いたので、今回はそれ以外の部分について気になった点をメモしておく。
- Collectionをアクティブにするのがめんどい
すぐ思いつく簡単な方法が通らなかったので検索。↑のdef recurLayerCollection(layerColl, collName)
は以下を参考にした。
https://blender.stackexchange.com/
Change active collection - UV unwrapのパラメータがラジアン
画面上はオイラーアングル?っぽいけどAPIではfloat0~1.xxまでとか書いている。ラジアンらしいのでmath.radians()
を使った。 - オブジェクトの選択解除どうする?
bpy.ops.object.select_all(action='DESELECT')
テストした インポートからの加工が一瞬で終わって少し感動したが、これで楽できると思ったら大間違いだった。
3.実際に使ってみた
PLATEAUのLOD1の建物をエリア単位でインポートしてみる。昨日手作業でインポートした大田区エリアは30ファイルあり、およそ1時間程度を要した。今回選択した目黒・品川エリアはファイル数は96と多いが自動化したのだからと期待は大きかった。
スクリプト実行でBlenderは即応答なしの状態へ入り不安に襲われる。タスクマネージャを起動してCPU,RAMの使用状況が変動していることからフリーズでは無いと判断。アニメを見たりして気を紛らわす
1時間後コンビニにでかけて帰ってきてもまだ応答がない。仕方がないのでパソコンの前でヨガを始める。呼吸に集中していると、時折PCのファンの回転数が上昇してBlenderの息吹を感じた。まだフリーズしていない。
ヨガが終わって買ってきたポテチを食べようとした頃にようやく完了した。開始からおよそ2時間40分、信じてよかった。
4.まとめ
今回は成功したからいいものの、失敗していたら相当落ち込む。一度にインポートするファイル数を減らすかコンソールにログを出力して正常に動作していることを確認できる仕組みを取り入れることができるのならやってみたい。
BlenderでPython使ってPLATEAUのモデルをランダム選択して結合
ぷちスタで作成したUE5のタイムアタックゲームを拡張しようとマップの作成を考えていたところ、PLATEAUという国のサービス?の存在を知り、23区のFBXデータをダウンロードした。解凍後のサイズは約9GBで結構でかい。
LOD2のモデルはテクスチャ付きだが加工難易度が高そうなのでLOD1のモデルを使用することにした。が、一つ一つはローポリの豆腐建築であってもUE5へインポートするとオブジェクト数が多すぎて応答なしが多発してうまくいかなかった。
Blenderで加工
とりあえず以下のような作業を機能を試しながらポチポチとやってみた。7回なのはテトリスのあれが7色だからなんとなく。
- ランダムに選択
- 選択したオブジェクトを結合
- マテリアルを追加
- 非表示にする
- 1に戻って7回繰り返す
一つのエリアは100個に分割されている。23区全体で14エリアで1400回もポチポチする気になれない。存在だけは知っていたBlender Pythonの出番かと、重い腰を上げて使ってみた。
Blender Pythonを使ってみる
Editor Type
をInfo
にするとこんな画面が出てくる。マウスやキーボードでBlenderを操作するとそのたびにログが出てくる。これをコピペでやればいいんだろうと最初は甘く見ていた。
すんなりいかなかった点
オブジェクトの結合
Ctr + J
をするのには複数選択したうえでActive
な状態のオブジェクトが必要、マウス無しでどうやるの?Brows Material to be linked
をクリックして選んでたけどどうすんの?
ググったらなんとかなった。
- セレクトしてるオブジェクトのうち最初の1つをターゲットにする
target = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = target
- 結合されたターゲットに対して
if target.data.materials: target.data.materials[0] = bpy.data.materials[sd] else: target.data.materials.append(bpy.data.materials[sd])
そんな感じでこんな感じになった。
まとめ
23区全体を処理しようとすると結局1400回近くFBXをBlenderへインポートしてスクリプトを走らせる作業があると思うとぞっとしてきた。それも自動化できたらいいなぁと思いつつも、時間がないのでエリアを縮小することする。