This commit is contained in:
Leon Liu 2025-08-18 01:10:56 +09:00
parent 07ccac9c2a
commit e83da3156b
45 changed files with 781 additions and 269 deletions

63
Cargo.lock generated
View File

@ -1502,6 +1502,28 @@ dependencies = [
"winit",
]
[[package]]
name = "big_space"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237c788129417fab794b74c8d67a7bda3a967e19f530d3b03e17e36576ffe6a"
dependencies = [
"bevy_app",
"bevy_color",
"bevy_ecs",
"bevy_input",
"bevy_log",
"bevy_math",
"bevy_platform",
"bevy_reflect",
"bevy_render",
"bevy_tasks",
"bevy_time",
"bevy_transform",
"bevy_utils",
"smallvec",
]
[[package]]
name = "bindgen"
version = "0.70.1"
@ -1984,6 +2006,25 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@ -4505,6 +4546,26 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "read-fonts"
version = "0.29.3"
@ -5106,9 +5167,11 @@ dependencies = [
"bevy",
"bevy-inspector-egui",
"bevy_panorbit_camera",
"big_space",
"chrono",
"futures",
"iyes_perf_ui",
"rayon",
"regex",
"reqwest 0.12.23",
"rhorizons",

View File

@ -7,9 +7,11 @@ edition = "2024"
bevy = "0.16"
bevy-inspector-egui = "0.33.1"
bevy_panorbit_camera = "0.26"
big_space = { version = "0.10.0", features = ["camera"] }
chrono = "0.4.41"
futures = "0.3.31"
iyes_perf_ui = "0.5.0"
rayon = "1.11.0"
regex = "1.11.1"
reqwest = { version = "0.12", features = ["json"] }
rhorizons = "0.5.0"

View File

@ -11,6 +11,10 @@
radius: 695700.0,
position: (0.0, 0.0, 0.0),
velocity: (0.0, 0.0, 0.0),
model: (
path: "sun.glb",
scale: (4.016625881e-2, 4.016625881e-2, 4.016625881e-2),
),
),
(
name: "Mercury",
@ -18,6 +22,10 @@
radius: 2439.4,
position: (-3.873030085256687e-01, -1.617241946342014e-01, 2.230772540124238e-02),
velocity: (5.024430196457658e-03, -2.474345331076241e-02, -2.482923861184285e-03),
model: (
path: "mercury.glb",
scale: (1.409040560e-4, 1.407730015e-4, 1.409040560e-4),
),
),
(
name: "Venus",
@ -25,6 +33,10 @@
radius: 6051.84,
position: (4.534187654737982e-01, 5.622160792960551e-01, -1.844136025242647e-02),
velocity: (-1.580627428004959e-02, 1.261006264179427e-02, 1.085217883057661e-03),
model: (
path: "venus.glb",
scale: (3.494008561e-4, 3.494008561e-4, 3.494008561e-4),
),
),
(
name: "Earth",
@ -32,6 +44,10 @@
radius: 6371.01,
position: (-1.786834409731047e-01, 9.669827953774551e-01, -5.109423915082682e-05),
velocity: (-1.720473858166942e-02, -3.193533189307208e-03, 5.457174067040888e-09),
model: (
path: "earth.glb",
scale: (3.686534474e-4, 3.674174368e-4, 3.686534474e-4),
),
),
(
name: "Mars",
@ -39,6 +55,10 @@
radius: 3389.92,
position: (-5.216858665681381e-01, 1.525234576802456e+00, 4.475559803760917e-02),
velocity: (-1.271183490340250e-02, -3.338839586395265e-03, 2.417788175776839e-04),
model: (
path: "mars.glb",
scale: (1.964640105e-4, 1.953076135e-4, 1.964640105e-4),
),
),
(
name: "Jupiter",
@ -46,6 +66,10 @@
radius: 69911.0,
position: (1.056033545576702e+00, 4.971452162023883e+00, -4.427806378454757e-02),
velocity: (-7.476272400979211e-03, 1.924466075080766e-03, 1.592575331437652e-04),
model: (
path: "jupiter.glb",
scale: (4.216764122e-3, 3.943203948e-3, 4.216764122e-3),
),
),
(
name: "Saturn",
@ -53,6 +77,10 @@
radius: 58232.0,
position: (9.461067271500818e+00, -1.764614720843175e+00, -3.458764004169705e-01),
velocity: (7.093807804551229e-04, 5.475097536527790e-03, -1.232345969963958e-04),
model: (
path: "saturn.glb",
scale: (1.828302280e-3, 1.649197191e-3, 1.828302280e-3),
),
),
(
name: "Uranus",
@ -60,6 +88,10 @@
radius: 25362.0,
position: (1.110362881512566e+01, 1.609448391218202e+01, -8.419833258524990e-02),
velocity: (-3.273728238819980e-03, 2.053528328895527e-03, 5.008589052969062e-05),
model: (
path: "uranus.glb",
scale: (1.486925757e-3, 1.452834462e-3, 1.486925757e-3),
),
),
(
name: "Neptune",
@ -67,6 +99,10 @@
radius: 24624.0,
position: (2.987992735576156e+01, -6.341879950443392e-01, -6.754997950415415e-01),
velocity: (3.941595250081164e-05, 3.160389775728832e-03, -6.636996572427530e-05),
model: (
path: "neptune.glb",
scale: (1.437890343e-3, 1.413329272e-3, 1.437890343e-3),
),
),
(
name: "Moon",
@ -74,6 +110,10 @@
radius: 1737.53,
position: (-1.776670337217189e-01, 9.646511868801392e-01, -2.560710414773878e-04),
velocity: (-1.666610342674751e-02, -2.935662211368250e-03, 2.214641479437884e-05),
model: (
path: "moon.glb",
scale: (1.003505604e-4, 1.003505604e-4, 1.003505604e-4),
),
),
(
name: "Phobos",
@ -81,6 +121,10 @@
radius: 11.1,
position: (-5.216607506283159e-01, 1.525290094552813e+00, 4.474552444674667e-02),
velocity: (-1.371531677510715e-02, -2.797359927923991e-03, 7.604209362929195e-04),
model: (
path: "phobos.glb",
scale: (3.111071419e-5, 2.177749957e-5, 2.728170512e-5),
),
),
(
name: "Deimos",
@ -88,6 +132,10 @@
radius: 6.2,
position: (-5.215476623931578e-01, 1.525280501692070e+00, 4.469744727198185e-02),
velocity: (-1.290124900899404e-02, -2.594527535740538e-03, 3.799855876623048e-04),
model: (
path: "deimos.glb",
scale: (3.572769492e-5, 2.336041507e-5, 2.748284533e-5),
),
),
(
name: "Io",
@ -95,6 +143,10 @@
radius: 1821.49,
position: (1.053815818667817e+00, 4.969715993299296e+00, -4.437108516416628e-02),
velocity: (-1.329530996368361e-03, -5.983335980023409e-03, -3.295420474951598e-05),
model: (
path: "io.glb",
scale: (1.056204637e-4, 1.048294944e-4, 1.050431092e-4),
),
),
(
name: "Europa",
@ -102,6 +154,10 @@
radius: 1560.8,
position: (1.060479105966296e+00, 4.971193788432235e+00, -4.418907806556983e-02),
velocity: (-6.967074192246404e-03, 9.896683771810605e-03, 4.296001484155991e-04),
model: (
path: "europa.glb",
scale: (9.021675214e-5, 9.003777086e-5, 9.008396592e-5),
),
),
(
name: "Ganymede",
@ -109,6 +165,10 @@
radius: 2631.2,
position: (1.062520398626604e+00, 4.968458682597362e+00, -4.429967587277176e-02),
velocity: (-4.841408463186764e-03, 7.634290231598638e-03, 4.147177169244575e-04),
model: (
path: "ganymede.glb",
scale: (1.519124053e-4, 1.519124053e-4, 1.519124053e-4),
),
),
(
name: "Callisto",
@ -116,6 +176,10 @@
radius: 2410.3,
position: (1.046927187861326e+00, 4.980213520221207e+00, -4.412635447845523e-02),
velocity: (-1.076662893789138e-02, -1.453153071657253e-03, 9.400054299331563e-06),
model: (
path: "callisto.glb",
scale: (1.391587430e-4, 1.391587430e-4, 1.391587430e-4),
),
),
(
name: "Mimas",
@ -123,6 +187,10 @@
radius: 198.8,
position: (9.462113317387443e+00, -1.764120396234732e+00, -3.462575454806998e-01),
velocity: (-3.600569225193193e-03, 1.195945377622506e-02, -3.321658418910402e-03),
model: (
path: "mimas.glb",
scale: (3.022084638e-5, 2.771941035e-5, 2.860654604e-5),
),
),
(
name: "Enceladus",
@ -130,6 +198,10 @@
radius: 252.3,
position: (9.462107070733344e+00, -1.763590731315564e+00, -3.465134777458668e-01),
velocity: (-4.799068161710644e-03, 9.904458072213898e-03, -1.910803547584920e-03),
model: (
path: "enceladus.glb",
scale: (1.481480831e-5, 1.433560737e-5, 1.451458593e-5),
),
),
(
name: "Tethys",
@ -137,6 +209,10 @@
radius: 536.3,
position: (9.463028141754283e+00, -1.764725184828637e+00, -3.460263238634043e-01),
velocity: (8.124741088132637e-04, 1.132977234261396e-02, -3.072248079372077e-03),
model: (
path: "tethys.glb",
scale: (3.129428660e-5, 3.059098162e-5, 3.070722960e-5),
),
),
(
name: "Dione",
@ -144,6 +220,10 @@
radius: 562.5,
position: (9.460669031566509e+00, -1.762393870120674e+00, -3.470001695782450e-01),
velocity: (-4.995042476514132e-03, 4.898468069198046e-03, 7.315317142172153e-04),
model: (
path: "dione.glb",
scale: (3.252791430e-5, 3.230852235e-5, 3.240667138e-5),
),
),
(
name: "Rhea",
@ -151,6 +231,10 @@
radius: 764.5,
position: (9.463441058546145e+00, -1.766997606737125e+00, -3.448366300385151e-01),
velocity: (4.307393238751094e-03, 8.273257376182423e-03, -1.932435874857194e-03),
model: (
path: "rhea.glb",
scale: (4.416729644e-5, 4.401718616e-5, 4.405760046e-5),
),
),
(
name: "Titan",
@ -158,6 +242,10 @@
radius: 2575.5,
position: (9.468440328490537e+00, -1.762284834421733e+00, -3.478122131916790e-01),
velocity: (-4.611400435334967e-04, 8.260265884102586e-03, -1.443082816825538e-03),
model: (
path: "titan.glb",
scale: (1.486763649e-4, 1.486371038e-4, 1.486549882e-4),
),
),
(
name: "Iapetus",
@ -165,6 +253,10 @@
radius: 734.5,
position: (9.454548058738080e+00, -1.786906158368561e+00, -3.394196336672040e-01),
velocity: (2.477125925502744e-03, 4.942920731685813e-03, -3.564738618756286e-04),
model: (
path: "iapetus.glb",
scale: (4.305300899e-5, 4.111311500e-5, 4.305300899e-5),
),
),
(
name: "Miranda",
@ -172,6 +264,10 @@
radius: 235.8,
position: (1.110338075481133e+01, 1.609470886437212e+01, -8.339848717695617e-02),
velocity: (2.787135511093017e-04, 1.302160355796409e-03, 1.366210183434322e-03),
model: (
path: "miranda.glb",
scale: (1.387950124e-5, 1.344648808e-5, 1.352154413e-5),
),
),
(
name: "Ariel",
@ -179,6 +275,10 @@
radius: 578.9,
position: (1.110453239435098e+01, 1.609416926126415e+01, -8.504238243640228e-02),
velocity: (-5.417334967749265e-03, 2.196892645955784e-03, -2.298152372242362e-03),
model: (
path: "ariel.glb",
scale: (3.354982618e-5, 3.335352449e-5, 3.336507143e-5),
),
),
(
name: "Umbriel",
@ -186,6 +286,10 @@
radius: 584.7,
position: (1.110253678329407e+01, 1.609490555702174e+01, -8.286998450094619e-02),
velocity: (-1.219602563560983e-03, 1.849034126664038e-03, 1.801100761916168e-03),
model: (
path: "umbriel.glb",
scale: (3.375767119e-5, 3.375767119e-5, 3.375767119e-5),
),
),
(
name: "Titania",
@ -193,6 +297,10 @@
radius: 788.9,
position: (1.110510884778205e+01, 1.609382177197045e+01, -8.661833178512783e-02),
velocity: (-5.032495225759578e-03, 2.280002002691279e-03, -1.088327315015861e-03),
model: (
path: "titania.glb",
scale: (4.554716361e-5, 4.554716361e-5, 4.554716361e-5),
),
),
(
name: "Oberon",
@ -200,6 +308,10 @@
radius: 761.4,
position: (1.110301190463219e+01, 1.609515778477002e+01, -8.040210462613612e-02),
velocity: (-1.519780917379062e-03, 1.720169702110850e-03, 3.926675849694972e-04),
model: (
path: "oberon.glb",
scale: (4.395945143e-5, 4.395945143e-5, 4.395945143e-5),
),
),
(
name: "Triton",
@ -207,6 +319,10 @@
radius: 1352.6,
position: (2.987802053077272e+01, -6.355569689698364e-01, -6.751627943073906e-01),
velocity: (-6.787804924900204e-04, 4.636375429506899e-03, 1.865945736548040e-03),
model: (
path: "triton.glb",
scale: (7.809240196e-5, 7.809240196e-5, 7.809240196e-5),
),
),
],
)

BIN
assets/models/ariel.glb Normal file

Binary file not shown.

BIN
assets/models/callisto.glb Normal file

Binary file not shown.

BIN
assets/models/ceres.glb Normal file

Binary file not shown.

BIN
assets/models/charon.glb Normal file

Binary file not shown.

BIN
assets/models/deimos.glb Normal file

Binary file not shown.

BIN
assets/models/dione.glb Normal file

Binary file not shown.

BIN
assets/models/earth.glb Normal file

Binary file not shown.

BIN
assets/models/enceladus.glb Normal file

Binary file not shown.

BIN
assets/models/eris.glb Normal file

Binary file not shown.

BIN
assets/models/europa.glb Normal file

Binary file not shown.

BIN
assets/models/ganymede.glb Normal file

Binary file not shown.

BIN
assets/models/hubble.glb Normal file

Binary file not shown.

BIN
assets/models/iapetus.glb Normal file

Binary file not shown.

BIN
assets/models/io.glb Normal file

Binary file not shown.

BIN
assets/models/iss.glb Normal file

Binary file not shown.

BIN
assets/models/jupiter.glb Normal file

Binary file not shown.

BIN
assets/models/makemake.glb Normal file

Binary file not shown.

BIN
assets/models/mars.glb Normal file

Binary file not shown.

BIN
assets/models/mercury.glb Normal file

Binary file not shown.

BIN
assets/models/mimas.glb Normal file

Binary file not shown.

BIN
assets/models/miranda.glb Normal file

Binary file not shown.

BIN
assets/models/moon.glb Normal file

Binary file not shown.

BIN
assets/models/neptune.glb Normal file

Binary file not shown.

BIN
assets/models/oberon.glb Normal file

Binary file not shown.

BIN
assets/models/phobos.glb Normal file

Binary file not shown.

BIN
assets/models/pluto.glb Normal file

Binary file not shown.

BIN
assets/models/rhea.glb Normal file

Binary file not shown.

BIN
assets/models/saturn.glb Normal file

Binary file not shown.

BIN
assets/models/sun.glb Normal file

Binary file not shown.

BIN
assets/models/tethys.glb Normal file

Binary file not shown.

BIN
assets/models/titan.glb Normal file

Binary file not shown.

BIN
assets/models/titania.glb Normal file

Binary file not shown.

BIN
assets/models/triton.glb Normal file

Binary file not shown.

BIN
assets/models/umbriel.glb Normal file

Binary file not shown.

BIN
assets/models/uranus.glb Normal file

Binary file not shown.

BIN
assets/models/venus.glb Normal file

Binary file not shown.

BIN
assets/models/voyager.glb Normal file

Binary file not shown.

30
assets/scale.txt Normal file
View File

@ -0,0 +1,30 @@
Applied scale to Deimos: 3.572769492e-5, 2.336041507e-5, 2.748284533e-5
Applied scale to Umbriel: 3.375767119e-5, 3.375767119e-5, 3.375767119e-5
Applied scale to Phobos: 3.111071419e-5, 2.177749957e-5, 2.728170512e-5
Applied scale to Titania: 4.554716361e-5, 4.554716361e-5, 4.554716361e-5
Applied scale to Oberon: 4.395945143e-5, 4.395945143e-5, 4.395945143e-5
Applied scale to Mercury: 1.409040560e-4, 1.407730015e-4, 1.409040560e-4
Applied scale to Callisto: 1.391587430e-4, 1.391587430e-4, 1.391587430e-4
Applied scale to Venus: 3.494008561e-4, 3.494008561e-4, 3.494008561e-4
Applied scale to Uranus: 1.486925757e-3, 1.452834462e-3, 1.486925757e-3
Applied scale to Ariel: 3.354982618e-5, 3.335352449e-5, 3.336507143e-5
Applied scale to Europa: 9.021675214e-5, 9.003777086e-5, 9.008396592e-5
Applied scale to Pluto: 6.860653230e-5, 6.860653230e-5, 6.860653230e-5
Applied scale to Enceladus: 1.481480831e-5, 1.433560737e-5, 1.451458593e-5
Applied scale to Dione: 3.252791430e-5, 3.230852235e-5, 3.240667138e-5
Applied scale to Titan: 1.486763649e-4, 1.486371038e-4, 1.486549882e-4
Applied scale to Miranda: 1.387950124e-5, 1.344648808e-5, 1.352154413e-5
Applied scale to Neptune: 1.437890343e-3, 1.413329272e-3, 1.437890343e-3
Applied scale to Moon: 1.003505604e-4, 1.003505604e-4, 1.003505604e-4
Applied scale to Rhea: 4.416729644e-5, 4.401718616e-5, 4.405760046e-5
Applied scale to Sol: 4.016625881e-2, 4.016625881e-2, 4.016625881e-2
Applied scale to Iapetus: 4.305300899e-5, 4.111311500e-5, 4.305300899e-5
Applied scale to Jupiter: 4.216764122e-3, 3.943203948e-3, 4.216764122e-3
Applied scale to Ganymede: 1.519124053e-4, 1.519124053e-4, 1.519124053e-4
Applied scale to Earth: 3.686534474e-4, 3.674174368e-4, 3.686534474e-4
Applied scale to Saturn: 1.828302280e-3, 1.649197191e-3, 1.828302280e-3
Applied scale to Mimas: 3.022084638e-5, 2.771941035e-5, 2.860654604e-5
Applied scale to Triton: 7.809240196e-5, 7.809240196e-5, 7.809240196e-5
Applied scale to Mars: 1.964640105e-4, 1.953076135e-4, 1.964640105e-4
Applied scale to Tethys: 3.129428660e-5, 3.059098162e-5, 3.070722960e-5
Applied scale to Io: 1.056204637e-4, 1.048294944e-4, 1.050431092e-4

154
examples/big_space.rs Normal file
View File

@ -0,0 +1,154 @@
//! This example demonstrates what floating point error in rendering looks like. You can press
//! space bar to smoothly switch between enabling and disabling the floating origin.
//!
//! Instead of disabling the plugin outright, this example simply moves the floating origin
//! independently of the camera, which is equivalent to what would happen when moving far from the
//! origin when not using this plugin.
use bevy::prelude::*;
use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(Update, (rotator_system, toggle_plugin))
.run();
}
/// You can put things really, really far away from the origin. The distance we use here is actually
/// quite small, because we want the mesh to still be visible when the floating origin is far from
/// the camera. If you go much further than this, the mesh will simply disappear in a *POOF* of
/// floating point error when we disable this plugin.
///
/// This plugin can function much further from the origin without any issues. Try setting this to:
/// `10_000_000_000_000_000` with the default i64 feature, or
/// `10_000_000_000_000_000_000_000_000_000_000_000_000` with the i128 feature.
const DISTANCE: GridPrecision = 2_000_000;
/// Move the floating origin back to the "true" origin when the user presses the spacebar to emulate
/// disabling the plugin. Normally you would make your active camera the floating origin to avoid
/// this issue.
fn toggle_plugin(
input: Res<ButtonInput<KeyCode>>,
grids: Grids,
mut text: Query<&mut Text>,
mut disabled: Local<bool>,
mut floating_origin: Query<(Entity, &mut CellCoord), With<FloatingOrigin>>,
) -> Result {
if input.just_pressed(KeyCode::Space) {
*disabled = !*disabled;
}
let this_grid = grids
.parent_grid(floating_origin.single().unwrap().0)
.unwrap();
let mut origin_cell = floating_origin.single_mut()?.1;
let index_max = DISTANCE / this_grid.cell_edge_length() as GridPrecision;
let increment = index_max / 100;
let msg = if *disabled {
if origin_cell.x > 0 {
origin_cell.x = 0.max(origin_cell.x - increment);
origin_cell.y = 0.max(origin_cell.y - increment);
origin_cell.z = 0.max(origin_cell.z - increment);
"Disabling..."
} else {
"Floating Origin Disabled"
}
} else if origin_cell.x < index_max {
origin_cell.x = index_max.min(origin_cell.x.saturating_add(increment));
origin_cell.y = index_max.min(origin_cell.y.saturating_add(increment));
origin_cell.z = index_max.min(origin_cell.z.saturating_add(increment));
"Enabling..."
} else {
"Floating Origin Enabled"
};
let dist =
index_max.saturating_sub(origin_cell.x) * this_grid.cell_edge_length() as GridPrecision;
let thousands = |num: GridPrecision| {
num.to_string()
.as_bytes()
.rchunks(3)
.rev()
.map(core::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(",") // separator
};
text.single_mut()?.0 = format!(
"Press Spacebar to toggle: {msg}\nCamera distance to floating origin: {}\nMesh distance from origin: {}",
thousands(dist),
thousands(DISTANCE)
);
Ok(())
}
#[derive(Component)]
struct Rotator;
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator>>) {
for mut transform in &mut query {
transform.rotate_y(time.delta_secs());
}
}
fn setup_ui(mut commands: Commands) {
commands.spawn((
Text::default(),
TextFont {
font_size: 30.0,
..default()
},
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_big_space_default(|root| {
let d = DISTANCE / root.grid().cell_edge_length() as GridPrecision;
let distant_grid_cell = CellCoord::new(d, d, d);
// Normally, we would put the floating origin on the camera. However in this example, we
// want to show what happens as the camera is far from the origin, to emulate what
// happens when this plugin isn't used.
root.spawn_spatial((distant_grid_cell, FloatingOrigin));
root.spawn_spatial((
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_scale(Vec3::splat(0.2)),
distant_grid_cell,
Rotator,
))
.with_child((
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_xyz(0.0, 0.0, 20.0),
));
// light
root.spawn_spatial((
DirectionalLight::default(),
Transform::from_xyz(4.0, -10.0, -4.0),
distant_grid_cell,
));
// camera
root.spawn_spatial((
Camera3d::default(),
Transform::from_xyz(8.0, 8.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
distant_grid_cell,
));
});
}

98
examples/model.rs Normal file
View File

@ -0,0 +1,98 @@
use bevy::{
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
prelude::*,
window::WindowMode,
};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
use solar_sim::InitialState;
fn camera(mut commands: Commands) {
commands.spawn((
PanOrbitCamera {
focus: Vec3::new(0., 0.0, 0.0),
zoom_lower_limit: SCALE,
radius: Some(SCALE * 10.),
..default()
},
Camera {
hdr: true,
clear_color: ClearColorConfig::Custom(Color::BLACK),
..default()
},
Projection::Perspective(PerspectiveProjection {
near: 1e-9, // Very close near plane for extreme zooming
..default()
}),
Tonemapping::TonyMcMapface,
Bloom::NATURAL,
));
commands.spawn(DirectionalLight {
shadows_enabled: true,
illuminance: 50_000.0,
..default()
});
}
const SCALE: f32 = 1.;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Load initial state from RON file
let initial_state_content = match std::fs::read_to_string("assets/initial_state.ron") {
Ok(content) => content,
Err(err) => {
error!("Failed to read initial_state.ron: {}", err);
return;
}
};
let initial_state: InitialState = match ron::from_str(&initial_state_content) {
Ok(state) => state,
Err(err) => {
error!("Failed to parse initial_state.ron: {}", err);
return;
}
};
initial_state
.bodies
.iter()
.enumerate()
.for_each(|(i, body)| {
commands
.spawn(Transform::from_translation(Vec3::new(
(i) as f32 * SCALE * 10 as f32,
0.,
0.,
)))
.with_children(|parent| {
let scene = asset_server.load(format!("models/{}#Scene0", body.model.path));
parent.spawn((
SceneRoot(scene),
Transform::from_scale(Vec3::new(
body.model.scale.0,
body.model.scale.1,
body.model.scale.2,
)),
));
});
});
}
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Solar Sim".to_string(),
mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary),
..default()
}),
..default()
}))
.add_plugins(EguiPlugin::default())
.add_plugins(WorldInspectorPlugin::new())
.add_plugins(PanOrbitCameraPlugin)
.add_systems(Startup, (camera, setup))
.run();
}

23
src/lib.rs Normal file
View File

@ -0,0 +1,23 @@
use serde::Deserialize;
// Data structures for deserializing initial_state.ron
#[derive(Deserialize)]
pub struct InitialState {
pub bodies: Vec<CelestialBodyData>,
}
#[derive(Deserialize)]
pub struct CelestialBodyData {
pub name: String,
pub mass: f64, // kg
pub radius: f64, // km
pub position: (f64, f64, f64), // AU
pub velocity: (f64, f64, f64), // AU/day
pub model: ModelInfo,
}
#[derive(Deserialize)]
pub struct ModelInfo {
pub path: String,
pub scale: (f32, f32, f32),
}

View File

@ -1,66 +1,55 @@
use bevy::{
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
input::mouse::{MouseScrollUnit, MouseWheel},
math::DVec3,
prelude::*,
window::{WindowMode, WindowResolution},
};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
use big_space::{
plugin::BigSpaceDefaultPlugins,
prelude::{BigSpaceCommands, FloatingOrigin},
};
use iyes_perf_ui::{PerfUiPlugin, prelude::*};
use serde::Deserialize;
use solar_sim::InitialState;
mod jpl_horizon;
// Data structures for deserializing initial_state.ron
#[derive(Deserialize)]
struct InitialState {
bodies: Vec<CelestialBodyData>,
}
#[derive(Deserialize)]
struct CelestialBodyData {
name: String,
mass: f64, // kg
radius: f64, // km
position: (f64, f64, f64), // AU
velocity: (f64, f64, f64), // AU/day
}
// Scaling factor to convert AU to game units
// Neptune is approximately 30.1 astronomical units (AU) from the Sun
// Point light effective range is about 10 game units, so 10 game units = ~30 AU
const AU_TO_GAME_UNITS: f64 = 0.3;
const AU_TO_GAME_UNITS: f64 = 1. / 3.;
// Unit wrapper types - all distances in AU
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct DistanceAu(pub f64);
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct PositionAu(pub DVec3);
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct VelocityAuPerDay(pub DVec3);
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct MassSolarMass(pub f64);
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Reflect)]
pub struct ObjectName(pub String);
// Component wrappers
#[derive(Component)]
#[derive(Component, Reflect)]
struct Position(PositionAu);
#[derive(Component)]
#[derive(Component, Reflect)]
struct Velocity(VelocityAuPerDay);
#[derive(Component)]
#[derive(Component, Reflect)]
struct Mass(MassSolarMass);
#[derive(Component)]
#[derive(Component, Reflect)]
struct Radius(DistanceAu);
#[derive(Component)]
#[derive(Component, Reflect)]
struct Name(ObjectName);
#[derive(Bundle)]
@ -74,6 +63,19 @@ struct ObjectBundle {
#[derive(Component)]
struct Star;
#[derive(Component)]
struct EmissiveEnhanced;
#[derive(Component)]
struct NeedsEmissiveEnhancement;
#[derive(Component)]
struct EmissiveEnhancementAttempts {
attempts: u32,
max_attempts: u32,
}
#[derive(Component)]
struct Earth;
@ -91,127 +93,58 @@ struct LabelVisible(bool);
#[derive(Component)]
struct Trackable {}
// Resource to track which entity the camera should follow
#[derive(Resource)]
struct CameraFollow {
struct Origin {
target: Option<Entity>,
distance: f32, // Current zoom distance from target
}
pub struct SolarRenderingPlugin;
impl Plugin for SolarRenderingPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(CameraFollow {
target: None,
distance: 10.0,
})
app.insert_resource(Origin { target: None })
.add_systems(Startup, (setup_rendering, setup_ui))
.add_systems(
FixedPostUpdate,
(
sync_radius_to_mesh,
sync_position_to_transform,
sync_name_labels,
manage_label_overlaps.after(sync_position_to_transform).after(sync_name_labels),
camera_follow_system.after(sync_position_to_transform),
manage_label_overlaps
.after(sync_position_to_transform)
.after(sync_name_labels),
),
)
.add_systems(PostUpdate, update_label_positions.after(manage_label_overlaps))
.add_systems(
PostUpdate,
(update_label_positions.after(manage_label_overlaps),),
)
.add_systems(
Update,
(
handle_label_clicks,
handle_scroll_zoom,
handle_speed_controls,
enhance_sun_emissive_with_retry,
),
);
}
}
fn sync_radius_to_mesh(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
query: Query<(Entity, &Radius, Option<&Star>, Option<&Mesh3d>), Changed<Radius>>,
) {
for (entity, radius, star, existing_mesh) in query.iter() {
// Convert AU to game units for rendering
let render_radius = radius.0.0 * AU_TO_GAME_UNITS;
// Create or update sphere mesh
let sphere_mesh = meshes.add(Sphere::new(render_radius as f32));
let material = materials.add(if star.is_none() {
StandardMaterial {
base_color: Color::WHITE,
..default()
}
} else {
StandardMaterial {
base_color: Color::WHITE, // Sun-like color for stars
emissive: LinearRgba::rgb(200.0, 200.0, 200.0),
..default()
}
});
if existing_mesh.is_none() {
// Add mesh and material components if they don't exist
commands
.entity(entity)
.insert((Mesh3d(sphere_mesh), MeshMaterial3d(material)));
} else {
// Update existing mesh
commands.entity(entity).insert(Mesh3d(sphere_mesh));
}
}
}
fn sync_position_to_transform(
mut commands: Commands,
mut query: Query<(Entity, &Position, Option<&mut Transform>), Changed<Position>>,
mut query: Query<(Entity, &Position, &mut Transform)>,
origin: Res<Origin>,
) {
for (entity, position, transform) in query.iter_mut() {
// Convert AU to game units for rendering
let scaled_position = position.0.0 * AU_TO_GAME_UNITS;
match transform {
Some(mut t) => {
// Update existing transform
t.translation = scaled_position.as_vec3();
}
None => {
// Insert a new Transform if it doesn't exist
commands
.entity(entity)
.insert(Transform::from_translation(scaled_position.as_vec3()));
}
};
// let offset = origin.target.map_or(DVec3::ZERO, |target| {
// query
// .get(target)
// .map_or(DVec3::ZERO, |(_, position, _)| position.0.0)
// });
let offset = DVec3::ZERO;
for (_, position, mut transform) in query.iter_mut() {
transform.translation = ((position.0.0 - offset) * AU_TO_GAME_UNITS).as_vec3();
}
}
fn setup_rendering(mut commands: Commands) {
// Spawn camera with pan/orbit/zoom controls
// Place it at a good distance to view the solar system
commands.spawn((
Camera3d::default(),
Camera {
hdr: true,
clear_color: ClearColorConfig::Custom(Color::BLACK),
..default()
},
Projection::Perspective(PerspectiveProjection {
near: 1e-9, // Very close near plane for extreme zooming
..default()
}),
Tonemapping::TonyMcMapface,
Transform::from_translation(Vec3::new(0., 0., 10.0)),
Bloom::NATURAL,
));
commands.spawn(PointLight {
color: Color::WHITE,
shadows_enabled: true,
..default()
});
commands.spawn(PerfUiAllEntries::default());
}
@ -310,7 +243,11 @@ fn manage_label_overlaps(
*entity_a
} else {
// If radii are equal, prefer alphabetically first name
if name_a < name_b { *entity_b } else { *entity_a }
if name_a < name_b {
*entity_b
} else {
*entity_a
}
};
// Hide the smaller object's label
@ -371,74 +308,23 @@ fn handle_label_clicks(
(Changed<Interaction>, With<ObjectLabel>),
>,
trackable_objects: Query<(&Transform, &Trackable, &Radius)>,
mut camera_query: Query<&mut Transform, (With<Camera>, Without<Trackable>)>,
mut camera_follow: ResMut<CameraFollow>,
mut camera: Single<&mut PanOrbitCamera>,
mut origin: ResMut<Origin>,
) {
for (interaction, label) in interaction_query.iter() {
if *interaction == Interaction::Pressed {
if let Ok((target_transform, _trackable, radius)) =
if let Ok((_target_transform, _trackable, radius)) =
trackable_objects.get(label.target_entity)
{
if let Ok(mut camera_transform) = camera_query.single_mut() {
let target_world_pos = target_transform.translation;
// Calculate appropriate distance based on entity radius (with some padding)
let entity_radius = (radius.0.0 * AU_TO_GAME_UNITS) as f32;
let desired_distance = entity_radius * 16.0; // 8x radius
// Position camera at desired distance from target
let camera_direction =
(camera_transform.translation - target_world_pos).normalize();
let new_camera_pos = target_world_pos + camera_direction * desired_distance;
camera_transform.translation = new_camera_pos;
camera_transform.look_at(target_world_pos, Vec3::Y);
// Set the follow target and initial distance
camera_follow.target = Some(label.target_entity);
camera_follow.distance = desired_distance;
camera.target_radius = entity_radius * 10.;
camera.zoom_lower_limit = entity_radius * 4.;
camera.target_focus = Vec3::ZERO;
origin.target = Some(label.target_entity);
}
}
}
}
}
fn camera_follow_system(
camera_follow: Res<CameraFollow>,
objects_query: Query<&Transform, With<Trackable>>,
mut camera_query: Query<&mut Transform, (With<Camera>, Without<Trackable>)>,
) {
if let Some(target_entity) = camera_follow.target {
if let Ok(target_transform) = objects_query.get(target_entity) {
if let Ok(mut camera_transform) = camera_query.single_mut() {
// Fixed camera direction relative to tracked object (stationary relative position)
let fixed_direction = Vec3::new(0.0, 0.0, 1.0);
// Position camera at the specified distance from target in fixed direction
let new_camera_pos =
target_transform.translation + fixed_direction * camera_follow.distance;
camera_transform.translation = new_camera_pos;
// Keep camera looking at the target entity
camera_transform.look_at(target_transform.translation, Vec3::Y);
}
}
}
}
fn handle_scroll_zoom(
mut scroll_events: EventReader<MouseWheel>,
mut camera_follow: ResMut<CameraFollow>,
) {
for event in scroll_events.read() {
let zoom_delta = match event.unit {
MouseScrollUnit::Line => event.y * 0.1,
MouseScrollUnit::Pixel => event.y * 0.01,
};
// Update zoom distance with limits
camera_follow.distance = (camera_follow.distance * (1.0 - zoom_delta)).clamp(1e-9, 100.0);
}
}
fn handle_speed_controls(
@ -456,13 +342,13 @@ fn handle_speed_controls(
}
}
// Speed up with + or = key (double current speed, max 64x)
// Speed up with + or = key (double current speed, max 32x)
if keyboard_input.just_pressed(KeyCode::Equal)
|| keyboard_input.just_pressed(KeyCode::NumpadAdd)
{
if !time.is_paused() {
let current_speed = time.relative_speed();
let new_speed = (current_speed * 2.0).min(64.0);
let new_speed = (current_speed * 2.0).min(32.0);
time.set_relative_speed(new_speed);
println!("Speed: {}x", new_speed);
}
@ -488,17 +374,130 @@ fn handle_speed_controls(
}
}
fn initialize_camera_follow(
earth_query: Query<Entity, (With<Earth>, With<Trackable>)>,
mut camera_follow: ResMut<CameraFollow>,
fn enhance_sun_emissive_with_retry(
mut commands: Commands,
mesh_query: Query<&MeshMaterial3d<StandardMaterial>>,
mut sun_query: Query<
(Entity, &Children, Option<&mut EmissiveEnhancementAttempts>),
(
With<Star>,
With<NeedsEmissiveEnhancement>,
Without<EmissiveEnhanced>,
),
>,
children_query: Query<&Children>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
if let Ok(earth) = earth_query.single() {
camera_follow.target = Some(earth);
camera_follow.distance = 1e-3;
for (sun_entity, children, attempts_opt) in sun_query.iter_mut() {
// Initialize attempts tracking if not present
let mut attempts = if let Some(mut att) = attempts_opt {
att.attempts += 1;
att.attempts
} else {
// First attempt, add the component
commands
.entity(sun_entity)
.insert(EmissiveEnhancementAttempts {
attempts: 1,
max_attempts: 300, // Try for ~5 seconds at 60fps
});
1
};
// Only try every 10 frames to avoid spam
if attempts % 10 != 0 {
continue;
}
info!("Attempting sun emissive enhancement (attempt {})", attempts);
// Try to enhance the sun's emissive material
let enhanced =
enhance_emissive_for_children(children, &children_query, &mesh_query, &mut materials);
if enhanced {
info!("Successfully enhanced sun emissive material!");
// Mark this sun as enhanced to avoid repeated processing
commands.entity(sun_entity).insert(EmissiveEnhanced);
commands
.entity(sun_entity)
.remove::<NeedsEmissiveEnhancement>();
commands
.entity(sun_entity)
.remove::<EmissiveEnhancementAttempts>();
} else if attempts >= 300 {
// Give up after max attempts
info!(
"Giving up on sun emissive enhancement after {} attempts",
attempts
);
commands
.entity(sun_entity)
.remove::<NeedsEmissiveEnhancement>();
commands
.entity(sun_entity)
.remove::<EmissiveEnhancementAttempts>();
}
}
}
fn setup_solar_system(mut commands: Commands) {
fn enhance_emissive_for_children(
children: &Children,
children_query: &Query<&Children>,
mesh_query: &Query<&MeshMaterial3d<StandardMaterial>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
) -> bool {
let mut enhanced_any = false;
for child in children.iter() {
// Try to enhance material for this child if it has a mesh
if let Ok(material_handle) = mesh_query.get(child) {
if let Some(material) = materials.get_mut(&material_handle.0) {
// Enhance emissive properties while preserving existing textures
// Set emissive to bright white to enhance the existing emissive texture
info!("Enhanced material for child entity!");
material.emissive = LinearRgba::rgb(200.0, 200.0, 200.0);
// Keep all existing textures (base_color_texture, emissive_texture, etc.)
enhanced_any = true;
}
}
// Recursively check this child's children
if let Ok(grandchildren) = children_query.get(child) {
if enhance_emissive_for_children(grandchildren, children_query, mesh_query, materials) {
enhanced_any = true;
}
}
}
enhanced_any
}
const AU_TO_KM: f64 = 149597870.691;
fn setup_solar_system(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_big_space_default(|commands| {
commands.spawn((
PanOrbitCamera {
zoom_lower_limit: 1e-6,
..default()
},
Camera {
hdr: true,
clear_color: ClearColorConfig::Custom(Color::BLACK),
..default()
},
Projection::Perspective(PerspectiveProjection {
near: 1e-8, // Very close near plane for extreme zooming
far: 1e-2,
..default()
}),
Tonemapping::TonyMcMapface,
Transform::from_translation(Vec3::new(0., 0., 10.0)),
Bloom::NATURAL,
FloatingOrigin,
));
// Load initial state from RON file
let initial_state_content = match std::fs::read_to_string("assets/initial_state.ron") {
Ok(content) => content,
@ -516,10 +515,9 @@ fn setup_solar_system(mut commands: Commands) {
}
};
// Spawn all celestial bodies from the initial state
for body_data in initial_state.bodies {
// Convert radius from km to AU
let radius_au = body_data.radius / 149597870.691; // km to AU conversion
let radius_au = body_data.radius / AU_TO_KM; // km to AU conversion
// Convert mass from kg to solar masses
let mass_solar = body_data.mass / SOLAR_MASS_KG;
@ -542,12 +540,32 @@ fn setup_solar_system(mut commands: Commands) {
radius: Radius(DistanceAu(radius_au)),
},
Trackable {},
Transform::default(),
Visibility::default(),
));
let scene_handle = asset_server.load(format!("models/{}#Scene0", body_data.model.path));
entity_commands.with_child((
SceneRoot(scene_handle.clone()),
Transform::from_scale(
Vec3::new(
body_data.model.scale.0,
body_data.model.scale.1,
body_data.model.scale.2,
) / AU_TO_KM as f32
* 10000.,
),
));
// Add special components for Sun and Earth
match body_data.name.as_str() {
"Sun" => {
entity_commands.insert(Star);
entity_commands.insert(NeedsEmissiveEnhancement);
entity_commands.insert(PointLight {
color: Color::WHITE,
shadows_enabled: true,
..default()
});
}
"Earth" => {
entity_commands.insert(Earth);
@ -557,6 +575,7 @@ fn setup_solar_system(mut commands: Commands) {
info!("Spawned celestial body: {}", body_data.name);
}
});
}
// High precision constants
@ -570,10 +589,9 @@ const K_GAUSS: f64 = 0.01720209895; // AU^(3/2) / (solar_mass^(1/2) × day)
// This gives G×M_sun in AU³/day² units, and since we use solar mass units (M_sun = 1), G_AU = k²
const G_AU: f64 = K_GAUSS * K_GAUSS;
const STEPS: usize = 100;
const STEPS: usize = 20; // Number of steps per fixed update
// If FixedUpdate is 64 Hz and you want 1 day per game second: DT = 1/(64*STEPS) day/step
const DT: f64 = 1.0 / (64.0 * STEPS as f64);
fn n_body(mut query: Query<(&Mass, &mut Position, &mut Velocity)>) {
// Pull data out once. This copy lets us integrate freely without borrow issues.
// (If you want zero copies, use a Resource scratch buffer and reuse it across frames.)
@ -655,7 +673,9 @@ fn n_body(mut query: Query<(&Mass, &mut Position, &mut Velocity)>) {
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "Solar Sim".to_string(),
mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary),
@ -663,8 +683,17 @@ fn main() {
..default()
}),
..default()
}))
})
.disable::<TransformPlugin>(),
)
.add_plugins(BigSpaceDefaultPlugins)
.add_plugins(EguiPlugin::default())
.add_plugins(PanOrbitCameraPlugin)
.register_type::<Name>()
.register_type::<Radius>()
.register_type::<DistanceAu>()
.register_type::<Mass>()
.register_type::<MassSolarMass>()
.add_plugins(WorldInspectorPlugin::new())
.add_plugins(SolarRenderingPlugin) // we want Bevy to measure these values for us:
.add_plugins(bevy::diagnostic::FrameTimeDiagnosticsPlugin::default())
@ -672,10 +701,7 @@ fn main() {
.add_plugins(bevy::diagnostic::SystemInformationDiagnosticsPlugin)
.add_plugins(bevy::render::diagnostic::RenderDiagnosticsPlugin)
.add_plugins(PerfUiPlugin)
.add_systems(
Startup,
(setup_solar_system, initialize_camera_follow).chain(),
)
.add_systems(FixedUpdate, n_body)
.add_systems(Startup, (setup_solar_system).chain())
.add_systems(FixedUpdate, n_body) // Use the optimized n-body system
.run();
}