Загрузка данных
# ============================================================
# НЕБЕСНЫЙ ГЛОБУС Ø1500 мм
# 24 ЛЕПЕСТКА
# ЗВЁЗДЫ ТОЛЬКО mag < 5.0
#
# Blender 4.x
# ============================================================
import bpy
import math
import csv
import io
import urllib.request
from mathutils import Vector
# ------------------------------------------------------------
# НАСТРОЙКИ
# ------------------------------------------------------------
DIAMETER_MM = 1500.0
RADIUS_MM = DIAMETER_MM / 2.0
NUM_GORES = 24
GORE_WIDTH_DEG = 360.0 / NUM_GORES
HALF_GORE_DEG = GORE_WIDTH_DEG / 2.0
MAG_LIMIT = 5.0
# URL официального HYG 4.1
HYG_URL = (
"https://raw.githubusercontent.com/astronexus/"
"HYG-Database/main/hyg/CURRENT/hyg_v41.csv"
)
# Размеры звёзд на 3D-сфере
STAR_MIN_MM = 1.0
STAR_MAX_MM = 5.0
# Звёзды на развёртке
FLAT_STAR_MIN_MM = 0.8
FLAT_STAR_MAX_MM = 4.5
# Количество сегментов маленьких сфер-звёзд
STAR_SEGMENTS = 6
STAR_RINGS = 4
# Развёртка:
# синусоидальная схема:
# x = R * sin(lambda) * cos(latitude)
# y = R * latitude
#
# Это даёт ширину лепестка:
# 2 * R * sin(7.5°) = 196.35 мм на экваторе.
# ------------------------------------------------------------
# ОЧИСТКА СЦЕНЫ
# ------------------------------------------------------------
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for datablocks in (
bpy.data.meshes,
bpy.data.curves,
bpy.data.materials,
bpy.data.cameras,
bpy.data.lights,
):
pass
# ------------------------------------------------------------
# КОЛЛЕКЦИИ
# ------------------------------------------------------------
def new_collection(name):
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
return col
COL_SPHERE = new_collection("01_SKY_SPHERE")
COL_STARS_3D = new_collection("02_STARS_MAG_LT_5")
COL_GORES = new_collection("03_GORES_24")
COL_FLAT_STARS = new_collection("04_FLAT_STARS_MAG_LT_5")
COL_CUT = new_collection("05_CUT_LINES")
COL_LABELS = new_collection("06_LABELS")
# ------------------------------------------------------------
# МАТЕРИАЛЫ
# ------------------------------------------------------------
def make_material(name, color, metallic=0.0, roughness=0.5):
mat = bpy.data.materials.new(name)
mat.diffuse_color = (*color, 1.0)
mat.metallic = metallic
mat.roughness = roughness
return mat
MAT_SPHERE = make_material(
"Sky sphere",
(0.005, 0.008, 0.02),
0.0,
0.8
)
MAT_STAR = make_material(
"Stars",
(1.0, 0.85, 0.45),
0.0,
0.25
)
MAT_GORE = make_material(
"Gore paper",
(0.025, 0.035, 0.07),
0.0,
0.9
)
MAT_FLAT_STAR = make_material(
"Printed stars",
(1.0, 0.8, 0.25),
0.0,
0.4
)
MAT_LINE = make_material(
"Cut lines",
(1.0, 0.15, 0.05),
0.0,
0.5
)
MAT_LABEL = make_material(
"Labels",
(0.8, 0.9, 1.0),
0.0,
0.5
)
# ------------------------------------------------------------
# ЗАГРУЗКА HYG
# ------------------------------------------------------------
print("Downloading HYG 4.1...")
request = urllib.request.Request(
HYG_URL,
headers={"User-Agent": "Blender-Star-Globe/1.0"}
)
with urllib.request.urlopen(request, timeout=60) as response:
data = response.read().decode("utf-8")
print("HYG downloaded:", len(data), "bytes")
# ------------------------------------------------------------
# ЧТЕНИЕ ЗВЁЗД
# ------------------------------------------------------------
stars = []
reader = csv.DictReader(io.StringIO(data))
for row in reader:
try:
ra = float(row["ra"])
dec = float(row["dec"])
mag = float(row["mag"])
except:
continue
# Главное условие:
# 5.0 НЕ включается
if not (mag < MAG_LIMIT):
continue
# Некорректные значения пропускаем
if not (-90.0 <= dec <= 90.0):
continue
stars.append({
"ra": ra,
"dec": dec,
"mag": mag,
"name": row.get("proper", ""),
"con": row.get("con", ""),
})
print("Stars with mag < 5.0:", len(stars))
# ------------------------------------------------------------
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ------------------------------------------------------------
def star_radius(mag, min_size=STAR_MIN_MM, max_size=STAR_MAX_MM):
"""
Чем ярче звезда, тем больше её диаметр.
Диапазон ограничен для удобства печати.
"""
# Нормировка примерно от -1.5 до 5
t = (5.0 - mag) / 6.5
t = max(0.0, min(1.0, t))
# Немного нелинейности
t = t ** 0.55
return min_size + t * (max_size - min_size)
def ra_dec_to_xyz(ra_hours, dec_deg, radius):
"""
RA в часах.
Dec в градусах.
X = направление RA 0
Y = RA 6h
Z = северный полюс
"""
ra = math.radians(ra_hours * 15.0)
dec = math.radians(dec_deg)
x = radius * math.cos(dec) * math.cos(ra)
y = radius * math.cos(dec) * math.sin(ra)
z = radius * math.sin(dec)
return Vector((x, y, z))
def normalize_ra_deg(deg):
"""
RA в диапазон 0...360.
"""
return deg % 360.0
# ------------------------------------------------------------
# 3D СФЕРА
# ------------------------------------------------------------
bpy.ops.mesh.primitive_uv_sphere_add(
segments=96,
ring_count=48,
radius=RADIUS_MM,
location=(0, 0, 0)
)
sphere = bpy.context.object
sphere.name = "SKY_SPHERE_DIAMETER_1500mm"
sphere.data.materials.append(MAT_SPHERE)
# Перемещаем в нужную коллекцию
for col in list(sphere.users_collection):
col.objects.unlink(sphere)
COL_SPHERE.objects.link(sphere)
# ------------------------------------------------------------
# СОЗДАНИЕ 3D ЗВЁЗД
# ------------------------------------------------------------
print("Creating 3D stars...")
for i, star in enumerate(stars):
pos = ra_dec_to_xyz(
star["ra"],
star["dec"],
RADIUS_MM + 1.0
)
diameter = star_radius(star["mag"])
radius = diameter / 2.0
bpy.ops.mesh.primitive_ico_sphere_add(
subdivisions=1,
radius=radius,
location=pos
)
obj = bpy.context.object
obj.name = f"STAR_{i:05d}_mag_{star['mag']:.2f}"
obj.data.materials.append(MAT_STAR)
# Перемещаем в коллекцию
for col in list(obj.users_collection):
col.objects.unlink(obj)
COL_STARS_3D.objects.link(obj)
# ------------------------------------------------------------
# ГЕОМЕТРИЯ ЛЕПЕСТКА
# ------------------------------------------------------------
def gore_xy(lat_deg, local_lon_deg):
"""
Синусоидальная развёртка.
lat = -90...90
local_lon = -7.5...7.5
x = R sin(lambda) cos(phi)
y = R phi
Все размеры в мм.
"""
phi = math.radians(lat_deg)
lam = math.radians(local_lon_deg)
x = RADIUS_MM * math.sin(lam) * math.cos(phi)
y = RADIUS_MM * phi
return x, y
def gore_width_at_lat(lat_deg):
"""
Полная ширина лепестка на заданной широте.
"""
phi = math.radians(lat_deg)
return (
2.0
* RADIUS_MM
* math.sin(math.radians(HALF_GORE_DEG))
* math.cos(phi)
)
# ------------------------------------------------------------
# СОЗДАНИЕ 24 ЛЕПЕСТКОВ
# ------------------------------------------------------------
print("Creating 24 gores...")
LAT_STEPS = 72
for gore_index in range(NUM_GORES):
# Центральная долгота лепестка
center_lon = gore_index * GORE_WIDTH_DEG
vertices = []
faces = []
# Левая и правая границы
for side in (-1, 1):
for j in range(LAT_STEPS + 1):
lat = -90.0 + (
180.0 * j / LAT_STEPS
)
x, y = gore_xy(
lat,
side * HALF_GORE_DEG
)
# Размещаем лепестки отдельно,
# чтобы их было удобно рассматривать.
spacing = 2200.0
offset_x = (
(gore_index % 6) * spacing
)
offset_y = (
(gore_index // 6) * 1900.0
)
vertices.append(
(
x + offset_x,
y + offset_y,
0
)
)
# Индексы:
# левая сторона 0...LAT_STEPS
# правая сторона LAT_STEPS+1...
right_start = LAT_STEPS + 1
for j in range(LAT_STEPS):
faces.append(
(
j,
j + 1,
right_start + j + 1,
right_start + j
)
)
mesh = bpy.data.meshes.new(
f"GORE_{gore_index+1:02d}_MESH"
)
mesh.from_pydata(
vertices,
[],
faces
)
mesh.update()
obj = bpy.data.objects.new(
f"GORE_{gore_index+1:02d}_15deg",
mesh
)
COL_GORES.objects.link(obj)
obj.data.materials.append(MAT_GORE)
# Записываем полезные параметры как Custom Properties
obj["gore_number"] = gore_index + 1
obj["center_longitude_deg"] = center_lon
obj["width_deg"] = GORE_WIDTH_DEG
obj["sphere_diameter_mm"] = DIAMETER_MM
# ------------------------------------------------------------
# ПЕРЕНОС ЗВЁЗД НА ЛЕПЕСТКИ
# ------------------------------------------------------------
print("Creating flat star positions...")
def create_flat_star_mesh(name, x, y, diameter):
r = diameter / 2.0
segments = 12
verts = []
for k in range(segments):
a = 2.0 * math.pi * k / segments
verts.append(
(
x + r * math.cos(a),
y + r * math.sin(a),
0.5
)
)
face = tuple(range(segments))
mesh = bpy.data.meshes.new(name + "_MESH")
mesh.from_pydata(
verts,
[],
[face]
)
mesh.update()
obj = bpy.data.objects.new(
name,
mesh
)
COL_FLAT_STARS.objects.link(obj)
obj.data.materials.append(MAT_FLAT_STAR)
return obj
for i, star in enumerate(stars):
ra_deg = star["ra"] * 15.0
dec = star["dec"]
# Номер лепестка.
# Границы идут по 15 градусов.
gore_index = int(
math.floor(
normalize_ra_deg(ra_deg)
/ GORE_WIDTH_DEG
)
)
if gore_index >= NUM_GORES:
gore_index = NUM_GORES - 1
center_lon = (
gore_index * GORE_WIDTH_DEG
+ HALF_GORE_DEG
)
local_lon = (
normalize_ra_deg(ra_deg)
- center_lon
)
# Если объект на границе,
# он может оказаться ровно на краю.
# Для бумажной версии это нормально.
x, y = gore_xy(
dec,
local_lon
)
# То же расположение лепестков,
# что использовалось выше.
spacing = 2200.0
offset_x = (
(gore_index % 6) * spacing
)
offset_y = (
(gore_index // 6) * 1900.0
)
diameter = star_radius(
star["mag"],
FLAT_STAR_MIN_MM,
FLAT_STAR_MAX_MM
)
obj = create_flat_star_mesh(
f"FLAT_STAR_{i:05d}_mag_{star['mag']:.2f}",
x + offset_x,
y + offset_y,
diameter
)
obj["magnitude"] = star["mag"]
obj["RA_hours"] = star["ra"]
obj["Dec_deg"] = star["dec"]
obj["constellation"] = star["con"]
# ------------------------------------------------------------
# ЛИНИИ РЕЗА ЛЕПЕСТКОВ
# ------------------------------------------------------------
def create_line_object(name, points, material):
curve = bpy.data.curves.new(
name + "_CURVE",
type='CURVE'
)
curve.dimensions = '3D'
curve.resolution_u = 1
curve.bevel_depth = 0.8
curve.bevel_resolution = 1
spline = curve.splines.new('POLY')
spline.points.add(len(points) - 1)
for p, co in zip(spline.points, points):
p.co = (
co[0],
co[1],
co[2],
1.0
)
obj = bpy.data.objects.new(
name,
curve
)
COL_CUT.objects.link(obj)
curve.materials.append(material)
return obj
# Границы всех лепестков
for gore_index in range(NUM_GORES):
spacing = 2200.0
ox = (
(gore_index % 6) * spacing
)
oy = (
(gore_index // 6) * 1900.0
)
for side in (-1, 1):
points = []
for j in range(LAT_STEPS + 1):
lat = -90.0 + (
180.0 * j / LAT_STEPS
)
x, y = gore_xy(
lat,
side * HALF_GORE_DEG
)
points.append(
(
x + ox,
y + oy,
1.5
)
)
create_line_object(
f"GORe_{gore_index+1:02d}_CUT_{side}",
points,
MAT_LINE
)
# ------------------------------------------------------------
# ЭКВАТОР И ПОЛЮСА НА ЛЕПЕСТКАХ
# ------------------------------------------------------------
# Экватор
for gore_index in range(NUM_GORES):
spacing = 2200.0
ox = (
(gore_index % 6) * spacing
)
oy = (
(gore_index // 6) * 1900.0
)
points = []
for k in range(21):
local_lon = (
-HALF_GORE_DEG
+ 2.0 * HALF_GORE_DEG * k / 20.0
)
x, y = gore_xy(
0.0,
local_lon
)
points.append(
(
x + ox,
y + oy,
1.0
)
)
create_line_object(
f"GORE_{gore_index+1:02d}_EQUATOR",
points,
MAT_LINE
)
# ------------------------------------------------------------
# НОМЕРА ЛЕПЕСТКОВ
# ------------------------------------------------------------
def add_text(
text,
location,
size=50.0
):
curve = bpy.data.curves.new(
"TEXT_" + text,
type='FONT'
)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
curve.size = size
curve.extrude = 0.2
obj = bpy.data.objects.new(
"LABEL_" + text,
curve
)
obj.location = location
COL_LABELS.objects.link(obj)
curve.materials.append(MAT_LABEL)
return obj
for gore_index in range(NUM_GORES):
spacing = 2200.0
ox = (
(gore_index % 6) * spacing
)
oy = (
(gore_index // 6) * 1900.0
)
add_text(
f"GORE {gore_index+1:02d}",
(
ox,
oy - RADIUS_MM - 80,
2.0
),
45.0
)
# ------------------------------------------------------------
# ИНФОРМАЦИОННЫЙ ОБЪЕКТ
# ------------------------------------------------------------
scene_info = add_text(
"CELESTIAL GLOBE 1500mm / MAG < 5.0 / 24 GORES",
(0, -1100, 0),
35.0
)
# ------------------------------------------------------------
# НАСТРОЙКИ СЦЕНЫ
# ------------------------------------------------------------
scene = bpy.context.scene
scene["Globe_Diameter_mm"] = DIAMETER_MM
scene["Globe_Radius_mm"] = RADIUS_MM
scene["Number_of_Gores"] = NUM_GORES
scene["Gore_Width_Deg"] = GORE_WIDTH_DEG
scene["Magnitude_Limit"] = MAG_LIMIT
scene["Magnitude_Filter"] = "mag < 5.0"
scene["Star_Catalog"] = "HYG Database 4.1"
scene["Epoch"] = "J2000.0"
# Единицы — миллиметры
scene.unit_settings.system = 'METRIC'
scene.unit_settings.length_unit = 'MILLIMETERS'
# ------------------------------------------------------------
# СОЗДАЁМ КАМЕРУ ДЛЯ ОБЗОРА
# ------------------------------------------------------------
bpy.ops.object.camera_add(
location=(0, -2600, 1800)
)
camera = bpy.context.object
camera.name = "CAMERA_PREVIEW"
camera.rotation_euler = (
math.radians(65),
0,
0
)
scene.camera = camera
# ------------------------------------------------------------
# СТАТИСТИКА
# ------------------------------------------------------------
print("")
print("============================================")
print("ГОТОВО")
print("============================================")
print(f"Диаметр сферы: {DIAMETER_MM} мм")
print(f"Радиус: {RADIUS_MM} мм")
print(f"Лепестков: {NUM_GORES}")
print(f"Ширина лепестка: {GORE_WIDTH_DEG}°")
print(f"Ширина на экваторе: "
f"{2*RADIUS_MM*math.sin(math.radians(HALF_GORE_DEG)):.2f} мм")
print(f"Фильтр звёзд: mag < {MAG_LIMIT}")
print(f"Звёзд: {len(stars)}")
print("============================================")
# ------------------------------------------------------------
# СОХРАНЕНИЕ BLEND
# ------------------------------------------------------------
filepath = bpy.path.abspath(
"//celestial_globe_1500mm_mag_lt_5_24_gores.blend"
)
bpy.ops.wm.save_as_mainfile(
filepath=filepath
)
print("")
print("BLEND СОХРАНЁН:")
print(filepath)