basis() derived `right` from `forward × world_up`, which degenerated to a zero vector when forward aligned with world-Y (pitch = ±π/2) — the cross product vanished, normalization produced garbage, and crossing the pole flipped the image 180°. Right now depends on yaw alone: Vec3::new(cos yaw, 0, -sin yaw). It is a unit vector in the y=0 plane for any yaw, so it never goes to zero regardless of pitch. In the original upper hemisphere (cp > 0) this is exactly the old value after normalization, so existing views are unchanged; it additionally stays perfectly horizontal for all pitch, fixing a subtle roll the cross-product form introduced below the equator. With the singularity gone, pitch is unclamped from [0.05, π-0.05] to [-π+0.05, π-0.05]: the camera can now cross both poles and reach below the disk plane without flipping. The ±0.05 margin only avoids the exact origin-axis points where forward = ∓Y and yaw becomes meaningless. yaw is also wrapped to (-π, π] on each drag so it cannot drift out of the UI slider range (which would snap the view when the slider is later touched); cos/sin periodicity makes the wrap visually lossless. The UI pitch slider range is widened to match. Stale "gimbal pole" comments that described the old restriction are corrected. basis() still returns (eye, forward, right, up), so the uniform bridge in plugin.rs and both shaders are untouched.
62 lines
3.6 KiB
Rust
62 lines
3.6 KiB
Rust
use bevy::prelude::*;
|
|
use bevy_egui::egui;
|
|
|
|
pub fn ui_system(
|
|
mut contexts: bevy_egui::EguiContexts,
|
|
mut params: ResMut<crate::params::BlackHoleParams>,
|
|
mut camera: ResMut<crate::camera::OrbitCamera>,
|
|
mut wants: ResMut<crate::camera::WantsPointer>,
|
|
) {
|
|
if let Ok(ctx) = contexts.ctx_mut() {
|
|
egui::Window::new("Controls")
|
|
.collapsible(true)
|
|
.default_pos([16.0, 16.0])
|
|
.show(ctx, |ui| {
|
|
egui::CollapsingHeader::new("Camera")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.add(egui::Slider::new(&mut camera.distance, 3.0..=200.0).text("Distance"));
|
|
ui.add(egui::Slider::new(&mut camera.yaw, -std::f32::consts::PI..=std::f32::consts::PI).text("Yaw"));
|
|
ui.add(egui::Slider::new(&mut camera.pitch, (-std::f32::consts::PI + 0.05)..=(std::f32::consts::PI - 0.05)).text("Pitch"));
|
|
ui.add(egui::Slider::new(&mut camera.fov, 0.3..=2.0).text("FOV"));
|
|
});
|
|
egui::CollapsingHeader::new("Black Hole")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.add(egui::Slider::new(&mut params.spin, 0.0..=1.0).text("Spin (χ)"));
|
|
ui.label(format!("ISCO (disk inner): {:.3}", crate::physics::kerr_isco(params.spin)));
|
|
ui.label(format!("Horizon r+: {:.3}", crate::physics::kerr_horizon(params.spin)));
|
|
});
|
|
egui::CollapsingHeader::new("Accretion Disk")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
// disk_inner removed — now spin-derived (see Black Hole section).
|
|
ui.add(egui::Slider::new(&mut params.disk_outer, 6.0..=40.0).text("Outer radius"));
|
|
ui.add(egui::Slider::new(&mut params.disk_tilt, 0.0..=std::f32::consts::PI).text("Tilt"));
|
|
ui.add(egui::Slider::new(&mut params.disk_brightness, 0.0..=3.0).text("Brightness"));
|
|
ui.add(egui::Slider::new(&mut params.disk_rotation_speed, 0.0..=3.0).text("Rotation speed"));
|
|
});
|
|
egui::CollapsingHeader::new("Doppler").show(ui, |ui| {
|
|
ui.checkbox(&mut params.doppler_enabled, "Enabled");
|
|
ui.add_enabled(params.doppler_enabled, egui::Slider::new(&mut params.doppler_strength, 0.0..=3.0).text("Strength"));
|
|
});
|
|
egui::CollapsingHeader::new("Renderer").show(ui, |ui| {
|
|
ui.add(egui::Slider::new(&mut params.steps, 50..=600).text("Steps"));
|
|
ui.add(egui::Slider::new(&mut params.render_scale, 0.25..=1.0).text("Render scale"));
|
|
});
|
|
egui::CollapsingHeader::new("Background").show(ui, |ui| {
|
|
ui.add(egui::Slider::new(&mut params.star_intensity, 0.0..=3.0).text("Star intensity"));
|
|
ui.add(egui::Slider::new(&mut params.skybox_intensity, 0.0..=3.0).text("Skybox intensity"));
|
|
});
|
|
egui::CollapsingHeader::new("Grid").show(ui, |ui| {
|
|
ui.checkbox(&mut params.grid_enabled, "Enabled");
|
|
ui.add_enabled(params.grid_enabled, egui::Slider::new(&mut params.grid_density, 0.1..=4.0).text("Density"));
|
|
});
|
|
});
|
|
// egui captures pointer when the cursor is over a window or being interacted with.
|
|
wants.0 = ctx.egui_wants_pointer_input();
|
|
} else {
|
|
wants.0 = false;
|
|
}
|
|
}
|