fix(galaxy): ensure spiral arms are always visible
Three fixes for circular/non-spiral galaxies produced by Randomize:
1. Raise twist range from [1.0, 6.0] to [4.0, 18.0] radians
- Old max (~0.95 turns) could barely curve; new range covers
0.6–2.9 full turns, matching real spiral galaxy morphology
- Default twist raised from 3.0 to 12.0 (~1.9 turns)
2. Scale angular scatter inversely with arm count
- Replaced hardcoded ±0.36 rad scatter with arm_scatter() that
uses 30% of the inter-arm gap (TAU / arms × 0.30)
- Single-arm disks use a generous 1.5 rad scatter
- Prevents high-arm-count disks from blurring into circles
3. Bias randomize_disk toward spiral-friendly values
- Arms weighted toward 2–4 (10%, 30%, 30%, 20%, 10% distribution)
- Twist floored at arms × 4.0 rad so multi-arm disks always wind
enough for distinct spiral structure
- Outer radius defaults to "auto" 60% of the time
This commit is contained in:
@@ -357,7 +357,7 @@ fn generate_disk(
|
|||||||
let r = inner + rng.gen::<f32>().powf(0.62) * span;
|
let r = inner + rng.gen::<f32>().powf(0.62) * span;
|
||||||
let angle = std::f32::consts::TAU * arm as f32 / arm_count as f32
|
let angle = std::f32::consts::TAU * arm as f32 / arm_count as f32
|
||||||
+ ((r - inner) / span) * disk.twist
|
+ ((r - inner) / span) * disk.twist
|
||||||
+ (rng.gen::<f32>() - 0.5) * 0.72;
|
+ (rng.gen::<f32>() - 0.5) * arm_scatter(arm_count);
|
||||||
let y = (rng.gen::<f32>() - 0.5) * 20.0;
|
let y = (rng.gen::<f32>() - 0.5) * 20.0;
|
||||||
let mut candidate = Vec3::new(angle.cos() * r, y, angle.sin() * r);
|
let mut candidate = Vec3::new(angle.cos() * r, y, angle.sin() * r);
|
||||||
|
|
||||||
@@ -491,6 +491,20 @@ fn generate_beam(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Angular scatter width per system, scaled to the inter-arm gap.
|
||||||
|
/// Each arm occupies `TAU / arm_count` radians; systems scatter within
|
||||||
|
/// ~30% of that gap so arms remain visually distinct regardless of
|
||||||
|
/// how many there are. Single-arm disks use a fixed wide scatter
|
||||||
|
/// since there are no adjacent arms to bleed into.
|
||||||
|
fn arm_scatter(arm_count: u32) -> f32 {
|
||||||
|
if arm_count <= 1 {
|
||||||
|
// No adjacent arms — generous scatter for a natural-looking blob.
|
||||||
|
1.5
|
||||||
|
} else {
|
||||||
|
std::f32::consts::TAU / arm_count as f32 * 0.30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Spacing relaxation curve — same shape as the previous single-pass
|
/// Spacing relaxation curve — same shape as the previous single-pass
|
||||||
/// generator: full spacing for the first 60 attempts, 82% until attempt
|
/// generator: full spacing for the first 60 attempts, 82% until attempt
|
||||||
/// 120, then 68% as a fallback to guarantee placement.
|
/// 120, then 68% as a fallback to guarantee placement.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const DEFAULT_CORE_RADIUS: f32 = 38.0;
|
|||||||
// keeping the same arms/twist as the docs prototype.
|
// keeping the same arms/twist as the docs prototype.
|
||||||
const DEFAULT_DISK_COUNT: usize = 100;
|
const DEFAULT_DISK_COUNT: usize = 100;
|
||||||
const DEFAULT_DISK_ARMS: u32 = 4;
|
const DEFAULT_DISK_ARMS: u32 = 4;
|
||||||
const DEFAULT_DISK_TWIST: f32 = 3.0;
|
const DEFAULT_DISK_TWIST: f32 = 12.0; // ~1.9 full turns
|
||||||
|
|
||||||
// Beam defaults: small population so the two polar columns read clearly
|
// Beam defaults: small population so the two polar columns read clearly
|
||||||
// without dominating the scene. Equal top/bottom for symmetry.
|
// without dominating the scene. Equal top/bottom for symmetry.
|
||||||
@@ -66,9 +66,9 @@ pub const DISK_COUNT_MAX: usize = 220;
|
|||||||
pub const DISK_COUNT_STEP: usize = 4;
|
pub const DISK_COUNT_STEP: usize = 4;
|
||||||
pub const DISK_ARMS_MIN: u32 = 1;
|
pub const DISK_ARMS_MIN: u32 = 1;
|
||||||
pub const DISK_ARMS_MAX: u32 = 6;
|
pub const DISK_ARMS_MAX: u32 = 6;
|
||||||
pub const DISK_TWIST_MIN: f32 = 1.0;
|
pub const DISK_TWIST_MIN: f32 = 4.0; // ~0.6 turns — bare minimum curvature
|
||||||
pub const DISK_TWIST_MAX: f32 = 6.0;
|
pub const DISK_TWIST_MAX: f32 = 18.0; // ~2.9 turns — tight logarithmic spiral
|
||||||
pub const DISK_TWIST_STEP: f32 = 0.2;
|
pub const DISK_TWIST_STEP: f32 = 0.4;
|
||||||
|
|
||||||
/// Max tilt is 45° — disks should never be parallel to beams.
|
/// Max tilt is 45° — disks should never be parallel to beams.
|
||||||
pub const DISK_TILT_MIN: f32 = 0.0;
|
pub const DISK_TILT_MIN: f32 = 0.0;
|
||||||
|
|||||||
@@ -1300,14 +1300,35 @@ fn spawn_scroll_contents_with_tabs(
|
|||||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Randomize all fields of a [`DiskParams`] to random valid values within
|
/// Randomize all fields of a [`DiskParams`] to random valid values within
|
||||||
/// their respective bounds, rounded to the UI step.
|
/// their respective bounds, rounded to the UI step. Biased toward
|
||||||
|
/// spiral-friendly combinations: arms are weighted toward 2–4, twist is
|
||||||
|
/// floored at `arms × 4.0` radians (ensuring at least ~0.6 turns per arm),
|
||||||
|
/// and outer radius defaults to "auto" most of the time so disks fill the
|
||||||
|
/// galaxy.
|
||||||
fn randomize_disk(rng: &mut impl rand::Rng) -> DiskParams {
|
fn randomize_disk(rng: &mut impl rand::Rng) -> DiskParams {
|
||||||
let mut disk = DiskParams::default();
|
let mut disk = DiskParams::default();
|
||||||
disk.count = rng.gen_range(DISK_COUNT_MIN..=DISK_COUNT_MAX);
|
disk.count = rng.gen_range(DISK_COUNT_MIN..=DISK_COUNT_MAX);
|
||||||
disk.count = (disk.count / DISK_COUNT_STEP) * DISK_COUNT_STEP;
|
disk.count = (disk.count / DISK_COUNT_STEP) * DISK_COUNT_STEP;
|
||||||
disk.arms = rng.gen_range(DISK_ARMS_MIN..=DISK_ARMS_MAX);
|
|
||||||
disk.twist = rng.gen_range(DISK_TWIST_MIN..=DISK_TWIST_MAX);
|
// Weight arms toward 2–4 for clear spiral structure. 1 and 5–6 are
|
||||||
|
// allowed but less likely.
|
||||||
|
disk.arms = match rng.gen_range(0..10) {
|
||||||
|
0 => 1,
|
||||||
|
1..=3 => 2,
|
||||||
|
4..=6 => 3,
|
||||||
|
7..=8 => 4,
|
||||||
|
9 => rng.gen_range(5..=DISK_ARMS_MAX),
|
||||||
|
_ => DISK_ARMS_MIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Floor twist at arms × 4.0 so multi-arm disks always wind enough to
|
||||||
|
// show distinct spiral structure. The floor ensures at least ~0.6 turns
|
||||||
|
// between adjacent arms.
|
||||||
|
let twist_floor = disk.arms as f32 * 4.0;
|
||||||
|
let effective_min = DISK_TWIST_MIN.max(twist_floor);
|
||||||
|
disk.twist = rng.gen_range(effective_min..=DISK_TWIST_MAX);
|
||||||
disk.twist = (disk.twist / DISK_TWIST_STEP).round() * DISK_TWIST_STEP;
|
disk.twist = (disk.twist / DISK_TWIST_STEP).round() * DISK_TWIST_STEP;
|
||||||
|
|
||||||
disk.tilt = rng.gen_range(DISK_TILT_MIN..=DISK_TILT_MAX);
|
disk.tilt = rng.gen_range(DISK_TILT_MIN..=DISK_TILT_MAX);
|
||||||
disk.tilt = (disk.tilt / DISK_TILT_STEP).round() * DISK_TILT_STEP;
|
disk.tilt = (disk.tilt / DISK_TILT_STEP).round() * DISK_TILT_STEP;
|
||||||
disk.rotation_offset = rng.gen_range(DISK_ROTATION_MIN..=DISK_ROTATION_MAX);
|
disk.rotation_offset = rng.gen_range(DISK_ROTATION_MIN..=DISK_ROTATION_MAX);
|
||||||
@@ -1319,8 +1340,8 @@ fn randomize_disk(rng: &mut impl rand::Rng) -> DiskParams {
|
|||||||
disk.outer_radius = rng.gen_range(DISK_OUTER_RADIUS_MIN..=DISK_OUTER_RADIUS_MAX);
|
disk.outer_radius = rng.gen_range(DISK_OUTER_RADIUS_MIN..=DISK_OUTER_RADIUS_MAX);
|
||||||
disk.outer_radius =
|
disk.outer_radius =
|
||||||
(disk.outer_radius / DISK_OUTER_RADIUS_STEP).round() * DISK_OUTER_RADIUS_STEP;
|
(disk.outer_radius / DISK_OUTER_RADIUS_STEP).round() * DISK_OUTER_RADIUS_STEP;
|
||||||
// Allow 0 (auto) ~50% of the time.
|
// Allow 0 (auto) ~60% of the time so disks fill the galaxy by default.
|
||||||
if rng.gen_bool(0.5) {
|
if rng.gen_bool(0.6) {
|
||||||
disk.outer_radius = 0.0;
|
disk.outer_radius = 0.0;
|
||||||
}
|
}
|
||||||
disk
|
disk
|
||||||
|
|||||||
Reference in New Issue
Block a user