use rusting_engine::prelude::*;
use rusting_engine::runtime::Bloom;
use std::f32::consts::{PI, TAU};
use std::sync::OnceLock;
const GM: f32 = 3000.0;
const STAR_SOFTENING: f32 = 4.0;
const CORE_SOFTENING: f32 = 6.0;
const FRICTION: f32 = 0.2;
const FRICTION_RANGE: f32 = 25.0;
const START_DISTANCE: f32 = 110.0;
const PERICENTER: f32 = 25.0;
const START_SPEED: f32 = 0.9;
const SAMPLE_DT: f32 = 1.0 / 60.0;
const PATH_SECONDS: f32 = 60.0;
const DISK_INNER: f32 = 3.0;
const DISK_OUTER: f32 = 40.0;
const BULGE: f32 = 0.12;
const GRAVITY: &str = r#"
vec3 pull(vec3 offset, float gm, float softening2) {
float d2 = dot(offset, offset) + softening2;
return offset * (gm * inversesqrt(d2 * d2 * d2));
}
void condition(inout PhysicsState body) {
vec4 setup = condition_params.values[0]; // x GM, y softening², z sample dt, w samples
float sample_at = clamp(pc.elapsed / setup.z, 0.0, setup.w - 1.001);
uint index = uint(sample_at);
// The cores sit at +core and -core: equal masses, still centre of mass.
vec3 core = mix(condition_params.values[1u + index].xyz,
condition_params.values[2u + index].xyz, fract(sample_at));
vec3 position = body.model[3].xyz;
vec3 acceleration = pull(position - core, setup.x, setup.y)
+ pull(position + core, setup.x, setup.y);
body.velocity.xyz -= acceleration * pc.dt;
}
"#;
fn star_count() -> usize {
std::env::args()
.nth(1)
.filter(|arg| !arg.starts_with("--"))
.map_or(100_000, |count| count.parse().expect("star count"))
}
fn portrait() -> bool {
std::env::args().any(|arg| arg == "--portrait")
}
fn noise(index: usize, channel: u64) -> f32 {
let mut value = (index as u64) ^ (channel << 56);
value = value.wrapping_add(0x9e37_79b9_7f4a_7c15);
value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
value ^= value >> 31;
(value >> 40) as f32 / (1u64 << 24) as f32
}
struct CorePath {
samples: Vec<[f32; 4]>,
start_velocity: [f32; 3],
}
fn core_path() -> &'static CorePath {
static PATH: OnceLock<CorePath> = OnceLock::new();
PATH.get_or_init(|| {
let mu = 2.0 * GM;
let speed = START_SPEED * (2.0 * mu / START_DISTANCE).sqrt();
let across = (2.0 * mu * PERICENTER).sqrt() / START_DISTANCE;
let inward = (speed * speed - across * across).sqrt();
let (mut x, mut z) = (START_DISTANCE, 0.0_f32);
let (mut vx, mut vz) = (-inward, across);
let start_velocity = [vx / 2.0, 0.0, vz / 2.0];
let substeps = 10;
let dt = SAMPLE_DT / substeps as f32;
let count = (PATH_SECONDS / SAMPLE_DT) as usize;
let mut samples = Vec::with_capacity(count);
for _ in 0..count {
samples.push([x / 2.0, 0.0, z / 2.0, 0.0]);
for _ in 0..substeps {
let r2 = x * x + z * z;
let d2 = r2 + CORE_SOFTENING * CORE_SOFTENING;
let pull = mu / (d2 * d2.sqrt());
let drag =
FRICTION * (-r2 / (FRICTION_RANGE * FRICTION_RANGE)).exp();
vx -= (pull * x + drag * vx) * dt;
vz -= (pull * z + drag * vz) * dt;
x += vx * dt;
z += vz * dt;
}
}
CorePath {
samples,
start_velocity,
}
})
}
fn circular_speed(radius: f32) -> f32 {
let d2 = radius * radius + STAR_SOFTENING * STAR_SOFTENING;
(GM * radius * radius / (d2 * d2.sqrt())).sqrt()
}
fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: [f32; 3], s: f32) -> [f32; 3] {
a.map(|axis| axis * s)
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn normalize(a: [f32; 3]) -> [f32; 3] {
scale(a, 1.0 / (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt())
}
fn glow(scene: &mut GameScene<'_>, emissive: [f32; 3]) -> Handle {
scene.create_material(MaterialAsset {
base_color: [0.0, 0.0, 0.0, 1.0],
roughness: 1.0,
emissive,
..MaterialAsset::default()
})
}
type Handle = rusting_engine::assets::Handle<MaterialAsset>;
struct Galaxy {
centre: [f32; 3],
velocity: [f32; 3],
normal: [f32; 3],
bulge: Handle,
inner: Handle,
arms: Handle,
nebula: Handle,
}
fn spawn_star(
scene: &mut GameScene<'_>,
galaxy: &Galaxy,
index: usize,
shape: &CubeSpawn,
) -> [f32; 3] {
let n = galaxy.normal;
let u = normalize(cross(n, [0.3, 0.1, 1.0]));
let w = cross(n, u);
let (offset, axis, material) = if noise(index, 0) < BULGE {
let radius = 0.5 + 6.0 * noise(index, 1).powi(2);
let (theta, z) = (TAU * noise(index, 2), 2.0 * noise(index, 3) - 1.0);
let ring = (1.0 - z * z).sqrt();
let offset = scale([ring * theta.cos(), z, ring * theta.sin()], radius);
let tip = [noise(index, 4) - 0.5, 1.0, noise(index, 5) - 0.5];
(offset, normalize(add(n, scale(tip, 0.8))), galaxy.bulge)
} else {
let radius =
DISK_INNER + (DISK_OUTER - DISK_INNER) * noise(index, 1).powf(1.3);
let in_arm = noise(index, 6) < 0.7;
let angle = if in_arm {
let arm = if noise(index, 7) < 0.5 { 0.0 } else { PI };
let jitter = (noise(index, 2) + noise(index, 8) - 1.0) * 0.6;
arm + (radius / DISK_INNER).ln() / 0.45 + jitter
} else {
TAU * noise(index, 2)
};
let height = (noise(index, 3) - 0.5) * 1.2;
let offset = add(
add(
scale(u, radius * angle.cos()),
scale(w, radius * angle.sin()),
),
scale(n, height),
);
let material = if radius < 9.0 {
galaxy.inner
} else if in_arm && noise(index, 9) < 0.05 {
galaxy.nebula
} else {
galaxy.arms
};
(offset, n, material)
};
let radius =
(offset[0] * offset[0] + offset[1] * offset[1] + offset[2] * offset[2])
.sqrt();
let along = normalize(cross(axis, offset));
let mut transform = Transform::new(add(galaxy.centre, offset))
.with_rotation(TAU * noise(index, 10), TAU * noise(index, 11), 0.0);
transform.scale = [0.25 + 0.3 * noise(index, 12); 3];
scene.spawn_cube_with_material(
format!("Star {index}"),
transform,
shape,
material,
);
add(galaxy.velocity, scale(along, circular_speed(radius)))
}
fn create(scene: &mut GameScene<'_>) {
scene.set_background_color([0.0, 0.0, 0.0, 1.0]);
scene.world().spawn(Bloom {
intensity: 0.45,
threshold: 1.2,
spread: 0.55,
});
let far = glow(scene, [1.2, 1.3, 1.6]);
let far_shape = CubeSpawn::new();
for index in 0..3000 {
let (u, v) = (noise(index, 20), noise(index, 21));
let (theta, z) = (TAU * u, 2.0 * v - 1.0);
let ring = (1.0 - z * z).sqrt();
let mut transform = Transform::new(
[ring * theta.cos(), z, ring * theta.sin()]
.map(|axis| axis * 900.0),
);
transform.scale = [0.6 + 1.2 * noise(index, 22); 3];
scene.spawn_cube_with_material(
format!("Far Star {index}"),
transform,
&far_shape,
far,
);
}
let path = core_path();
let start = path.samples[0];
let start = [start[0], start[1], start[2]];
let velocity = path.start_velocity;
let galaxies = [
Galaxy {
centre: start,
velocity,
normal: [0.0, -1.0, 0.0],
bulge: glow(scene, [1.5, 1.15, 0.7]),
inner: glow(scene, [1.1, 0.85, 0.55]),
arms: glow(scene, [0.3, 0.5, 1.4]),
nebula: glow(scene, [1.6, 0.3, 0.6]),
},
Galaxy {
centre: scale(start, -1.0),
velocity: scale(velocity, -1.0),
normal: normalize([0.4, -0.5, 0.75]),
bulge: glow(scene, [1.5, 1.05, 0.8]),
inner: glow(scene, [1.1, 0.75, 0.6]),
arms: glow(scene, [0.75, 0.35, 1.4]),
nebula: glow(scene, [1.6, 0.35, 0.35]),
},
];
let shape = CubeSpawn::new().class("galaxy_star");
let count = star_count();
let velocities: Vec<_> = (0..count)
.map(|index| spawn_star(scene, &galaxies[index % 2], index, &shape))
.collect();
scene.apply_gpu_physics_to_class(
"galaxy_star",
&GpuBodySettings {
solver: PhysicsSolver::NoCollision,
rigid_body: RigidBody {
gravity_scale: 0.0,
..RigidBody::default()
},
..GpuBodySettings::default()
},
);
for (index, velocity) in velocities.into_iter().enumerate() {
scene.set_linear_velocity(&format!("Star {index}"), velocity);
}
println!("galaxy collision with {count} stars");
}
fn update(scene: &mut GameScene<'_>, time: &FrameTime) {
scene.once("create_galaxy_collision", create);
let path = core_path();
let mut params = Vec::with_capacity(path.samples.len() + 1);
params.push([
GM,
STAR_SOFTENING * STAR_SOFTENING,
SAMPLE_DT,
path.samples.len() as f32,
]);
params.extend_from_slice(&path.samples);
scene.set_gpu_condition_shaders(vec![GpuConditionShader {
events: Vec::new(),
glsl: GRAVITY.into(),
params,
events_per_body: 1,
}]);
let t = time.elapsed_seconds();
let close = (t / 30.0).clamp(0.0, 1.0);
let ease = close * close * (3.0 - 2.0 * close);
let reach = if portrait() { 1.9 } else { 1.0 };
let distance = reach * (195.0 - 20.0 * ease);
let angle = -1.9 + 0.035 * t;
let elevation = 1.0 - 0.45 * ease;
let position = [
distance * elevation.cos() * angle.cos(),
distance * elevation.sin(),
distance * elevation.cos() * angle.sin(),
];
scene
.object("Game Camera")
.set_position(position)
.look_at([0.0; 3]);
}
rusting_game!("testGame/build/main.rscene.bin", update);