ATMWRIによるUE5の記録

Unreal Engine 5 を使い始めました

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で星空を作った。