ATMWRIによるUE5の記録

Unreal Engine 5 を使い始めました

Unreal Engine 5とpythonで星空を作る

blenderで星空を作ったものの、UE5で使用するには使い勝手が悪かったのでUnreal Pythonを使用してUE5で星空を作ってキューブマップにした話

1.目標

  1. 9000個の星を生成
  2. CubeMapテクスチャ作成
  3. 天球用Sphereのマテリアル作成

2.結果

星空をキャプチャしたCubemapでSkydome作成

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の作成
Cube Render Targetを作成してワールドに配置すると周囲をキャプチャしてくれる。
Cube Render Targetの設定
Mip Gen SettingsをNoMipmapsに、Size X初期値512では解像度が足りなかったので最終的には2048に設定した。 Create Static Textureでテクスチャ化できる
Cube Render Targetからテクスチャの作成

skydome用マテリアルの作成

skydome用のマテリアル
[ue4 skydome]で検索してヒットした動画を参考にして、用意したCubemapを使用、地軸の傾き用パラメータと回転用のパラメータを作成した。

5.感想

blenderでは見た目等級を5にして星が2000個を超えたあたりで、処理時間を待てず離席して休憩するレベルだったが、UE5は見た目等級を最大にして約9000個の星を生成しても処理時間は1分も掛からなかったのには驚き。 ポストプロセスで星をキラキラさせたりも軽かった。
星空作るのに約1週間blenderunreal enginepythonで遊んだが、何かオリジナリティーのある事したいなぁ・・・面倒な作業といえばRig周り作業、リターゲティングとかもpythonでサクッとできたらいいことあるかも?

Unreal Pythonを使ったオブジェクトの配置とマテリアルの設定まで

UEでpython使ってみた。という記事はヒットするけれど、その先のちょっとした使い方までとなると検索力不足のためか中々見つからなかった。『UE5で星空を作る』の前段階としてUnreal Pythonの始め方からオブジェクトの配置・マテリアルの設定までを記録しておく

0. Unreal Pythonの始め方

ハローワールド

Output LogのドロップダウンメニューからPythonを選択すると使える

UE5では最初からpythonプラグインは有効になっていたっぽい?

ファイルを実行する準備

pythonスクリプトを作成するディレクトリを適当な場所に作成してAdditional Pathsへパスを追加する。この時Developer Modeにチェックを入れてstubファイルを生成するとTextエディタのオートコンプリートに使用できる

Project Settings -> Plugins -> Pythonでパスの追加

VScodeの設定

VScodeの設定でpythonオートコンプリートにUE5が生成したstubのpathを指定する

VScodeでpreferences -> settings [python auto] とかで検索

ファイルのimportとreload

import [ファイル名(py抜き)]でインポートとスクリプトの実行がされる。ファイルを編集を反映させるにはimportlibreloadが必要になる

importlibのreload関数?が必要

1. 目標

  1. コンテンツブラウザからアセットを選択
  2. 選択したアクターをワールドに配置
  3. 配置したアクターのマテリアルを設定

2. 結果

unreal pythonで生成したアクター

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.目標

  • 作成した星空のテクスチャをUE5で使用する
  • blender Pythonに慣れる

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 typeinforにして作業したlogから推測できるが、アウトライナー周辺は例外で面倒だった。

既存のMaterialを削除する

Blender FileのMaterialsを削除する。マウス操作ではアウトライナー上でドロップダウンメニューから選択して削除可能だが、infoは外れでほぼノーヒントbpy.context.blend_dataを探すことになった。

blenderfileのMaterials

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といったシンプルなものを作成した。

pythonで作成したシェーダーノード

  1. nodeを有効
  2. 初期ノードを一旦clear (デフォルトでPrincipled BSDFがあるので)
  3. Material Output, Emissionを作成してピンを接続
  4. BlackBodyを作成してEmissionとピンを接続
  5. 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の事らしい。

出来たもの

手順

Font Material動くグラデーションカラー
1. マテリアルを作成してUI用にする
- MaterialのDetailsからMaterial DomainUser Interfaceに変更 - Blend ModeTranslucentに変更

  1. TexCoordノードとPannerを使用してUVを動かす
    • PannerのDetailsにあるSpeed X, Speed Yへ直接入力か、Speedへパラメータノードを接続する
  2. 傾きを付ける
    • CustomRotatorノードを追加
    • Rotation Angleへ定数ノードを接続
  3. Texture Sampleで動かしたいテクスチャを設置
  4. 見た目の加工
    • 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_dirFPXAssetsは綴りを間違えているがあえて放置している。

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')

テストした

Test用に用意したfbxファイルx4をblenderpythonでインポート
インポートからの加工が一瞬で終わって少し感動したが、これで楽できると思ったら大間違いだった。

3.実際に使ってみた

PLATEAUのLOD1の建物をエリア単位でインポートしてみる。昨日手作業でインポートした大田区エリアは30ファイルあり、およそ1時間程度を要した。今回選択した目黒・品川エリアはファイル数は96と多いが自動化したのだからと期待は大きかった。

スクリプト実行でBlenderは即応答なしの状態へ入り不安に襲われる。タスクマネージャを起動してCPU,RAMの使用状況が変動していることからフリーズでは無いと判断。アニメを見たりして気を紛らわす

1時間後コンビニにでかけて帰ってきてもまだ応答がない。仕方がないのでパソコンの前でヨガを始める。呼吸に集中していると、時折PCのファンの回転数が上昇してBlenderの息吹を感じた。まだフリーズしていない。

ヨガが終わって買ってきたポテチを食べようとした頃にようやく完了した。開始からおよそ2時間40分、信じてよかった。

Blenderで96ファイル分のモデルをインポートして処理した結果

4.まとめ

今回は成功したからいいものの、失敗していたら相当落ち込む。一度にインポートするファイル数を減らすかコンソールにログを出力して正常に動作していることを確認できる仕組みを取り入れることができるのならやってみたい。

BlenderでPython使ってPLATEAUのモデルをランダム選択して結合

ぷちスタで作成したUE5のタイムアタックゲームを拡張しようとマップの作成を考えていたところ、PLATEAUという国のサービス?の存在を知り、23区のFBXデータをダウンロードした。解凍後のサイズは約9GBで結構でかい。
LOD2のモデルはテクスチャ付きだが加工難易度が高そうなのでLOD1のモデルを使用することにした。が、一つ一つはローポリの豆腐建築であってもUE5へインポートするとオブジェクト数が多すぎて応答なしが多発してうまくいかなかった。

Blenderで加工

とりあえず以下のような作業を機能を試しながらポチポチとやってみた。7回なのはテトリスのあれが7色だからなんとなく。

  1. ランダムに選択
  2. 選択したオブジェクトを結合
  3. マテリアルを追加
  4. 非表示にする
  5. 1に戻って7回繰り返す

PLATEAU 23区マップをこんな風に加工した

一つのエリアは100個に分割されている。23区全体で14エリアで1400回もポチポチする気になれない。存在だけは知っていたBlender Pythonの出番かと、重い腰を上げて使ってみた。

Blender Pythonを使ってみる

Editor TypeInfoにするとこんな画面が出てくる。マウスやキーボードでBlenderを操作するとそのたびにログが出てくる。これをコピペでやればいいんだろうと最初は甘く見ていた。

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])

そんな感じでこんな感じになった。

ランダムセレクトして結合、マテリアルのセットを7回繰り返すスクリプトのテスト

まとめ

23区全体を処理しようとすると結局1400回近くFBXをBlenderへインポートしてスクリプトを走らせる作業があると思うとぞっとしてきた。それも自動化できたらいいなぁと思いつつも、時間がないのでエリアを縮小することする。