@tool
class_name StarField
extends MultiMeshInstance3D
@export var catalog_path: String = "res://addons/sky3d_extended/data/bright_stars.dat"
@export_range(0.0, 6.5) var limiting_magnitude: float = 6.5
@export var scintillation_strength: float = 0.3
@export var star_distance: float = 3500.0
const STAR_PSF_SHADER = preload("res://addons/sky3d_extended/shaders/star_psf.gdshader")
const PSF_TEXTURE = preload("res://addons/sky3d_extended/assets/star_psf.png")
func _ready() -> void:
_build_star_mesh()
func _build_star_mesh() -> void:
var file := FileAccess.open(catalog_path, FileAccess.READ)
if not file:
push_warning("StarField: Cannot open " + catalog_path)
return
var header := file.get_line()
var stars: Array[Dictionary] = []
while not file.eof_reached():
var line := file.get_line().strip_edges()
if line.is_empty():
continue
var parts := line.split(",")
if parts.size() < 4:
continue
var mag := parts[2].to_float()
if mag > limiting_magnitude:
continue
stars.append({
"ra": parts[0].to_float(),
"dec": parts[1].to_float(),
"mag": mag,
"bv": parts[3].to_float(),
})
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.use_custom_data = true
mm.instance_count = stars.size()
var quad := QuadMesh.new()
quad.size = Vector2(20, 20)
mm.mesh = quad
for i in stars.size():
var s := stars[i]
var ra_rad: float = s["ra"] * TAU / 24.0
var dec_rad: float = deg_to_rad(s["dec"])
var x := star_distance * cos(dec_rad) * cos(ra_rad)
var y := star_distance * sin(dec_rad)
var z := star_distance * cos(dec_rad) * sin(ra_rad)
var t := Transform3D()
t.origin = Vector3(x, y, z)
mm.set_instance_transform(i, t)
var seed_val := fmod(s["ra"] * 1000.0 + s["dec"] * 100.0, 1.0)
mm.set_instance_custom_data(i, Color(s["mag"], s["bv"], seed_val, 0.0))
multimesh = mm
var mat := ShaderMaterial.new()
mat.shader = STAR_PSF_SHADER
mat.set_shader_parameter("scintillation_strength", scintillation_strength)
mat.set_shader_parameter("psf_texture", PSF_TEXTURE)
material_override = mat
print("StarField: Loaded %d stars" % stars.size())
## Rotate the MultiMesh to match sidereal time for smooth per-frame star rotation.
func update_rotation(sidereal_rotation: float, tilt: float) -> void:
var t := Basis.from_euler(Vector3(tilt, 0.0, 0.0), EULER_ORDER_XYZ)
var r := Basis.from_euler(Vector3(0.0, sidereal_rotation, 0.0), EULER_ORDER_XYZ)
transform = Transform3D(r * t, Vector3.ZERO)
func set_sky_brightness(brightness: float) -> void:
visible = brightness < 0.3