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