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でサクッとできたらいいことあるかも?