use std::sync::atomic::{AtomicU32, Ordering};
use oxijolt::*;
use super::frame::{Frame, UpPolicy};
use super::geometry::{Block, Field, Surface};
use crate::common::math::{add, rotate, rvec3, scale, sub, vec3, V3};
use crate::common::walker::{
capsule, chunk_pose, fixture_world, flat_terrain, up_at, Layers, CENTRE_UP, R, RADIUS,
REST_HEIGHT,
};
use crate::common::{quat_about, Groups};
pub const SAMPLES: usize = 33;
pub const HIGH_LEDGE_DROP: f64 = 2.0;
pub static FIELD_BITS_OVERRIDE: AtomicU32 = AtomicU32::new(0);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Profile {
Flat,
Rise30,
Descent44_5,
}
impl Profile {
pub fn height(self, x: f64) -> f64 {
let tan = match self {
Self::Flat => return 0.0,
Self::Rise30 => 30.0_f64.to_radians().tan(),
Self::Descent44_5 => 44.5_f64.to_radians().tan(),
};
tan * (x.clamp(-6.0, 6.0) + 6.0)
}
pub fn name(self) -> &'static str {
match self {
Self::Flat => "flat",
Self::Rise30 => "rise30",
Self::Descent44_5 => "descent44.5",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Split {
Pair,
Raised,
Continuous,
Compound,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SceneKey {
Plane { deg10: u16, bits: u8, tilted: bool },
Ramp { deg10: u16, tilted: bool },
Ridge { deg10: u16, tilted: bool },
Step { height_mm: u16, rounded: bool },
Ledge { drop_mm: u16, rounded: bool },
Wall { thick: bool },
BoxRamp { deg10: u16, tilted: bool },
Flat,
Seam { profile: Profile, split: Split },
RadialSpiral {
deg10: u16,
spacing_mm: u16,
bits: u8,
},
RadialCrest { deg10: u16 },
RadialStep { height_mm: u16 },
RadialSeam,
RadialWall,
HighLedgeBesideWall { face_mm: u16, lean_cdeg: i16 },
RadialHighLedgeBesideWall { face_mm: u16, lean_cdeg: i16 },
}
impl SceneKey {
pub fn plane(deg: f64) -> Self {
Self::Plane {
deg10: tenths(deg),
bits: 16,
tilted: false,
}
}
pub fn is_radial(self) -> bool {
matches!(
self,
Self::RadialSpiral { .. }
| Self::RadialCrest { .. }
| Self::RadialStep { .. }
| Self::RadialSeam
| Self::RadialWall
| Self::RadialHighLedgeBesideWall { .. }
)
}
}
pub fn tenths(deg: f64) -> u16 {
(deg * 10.0).round() as u16
}
fn degrees(deg10: u16) -> f64 {
f64::from(deg10) / 10.0
}
pub struct Scene {
pub world: PhysicsWorld,
pub layers: Layers,
pub frame: Frame,
pub up: UpPolicy,
pub surface: Surface,
pub terrain: Vec<BodyId>,
pub actor: BodyId,
}
impl Scene {
pub fn build(key: SceneKey) -> Self {
Self::build_with_threads(key, 1)
}
pub fn build_with_threads(key: SceneKey, worker_threads: u32) -> Self {
let frame = match key {
SceneKey::Plane { tilted: true, .. }
| SceneKey::Ramp { tilted: true, .. }
| SceneKey::Ridge { tilted: true, .. }
| SceneKey::BoxRamp { tilted: true, .. } => Frame::tilted(),
_ => Frame::IDENTITY,
};
let (world, layers) = fixture_world(worker_threads);
let up = if key.is_radial() {
UpPolicy::Radial
} else {
UpPolicy::Frame(frame)
};
let mut builder = Builder {
world,
layers,
frame,
surface: Surface::default(),
terrain: Vec::new(),
};
builder.add(key);
let actor = builder.actor();
builder.world.optimize_broad_phase();
Self {
world: builder.world,
layers: builder.layers,
frame,
up,
surface: builder.surface,
terrain: builder.terrain,
actor,
}
}
pub fn from_world(world: PhysicsWorld, layers: Layers) -> Self {
let mut builder = Builder {
world,
layers,
frame: Frame::IDENTITY,
surface: Surface::default(),
terrain: Vec::new(),
};
let actor = builder.actor();
Self {
world: builder.world,
layers,
frame: Frame::IDENTITY,
up: UpPolicy::Radial,
surface: builder.surface,
terrain: builder.terrain,
actor,
}
}
pub fn place_actor(&mut self, origin: V3, rotation: Quat) {
self.world
.body_mut(self.actor)
.unwrap()
.set_position_and_rotation(rvec3(origin), rotation, Activation::DontActivate)
.unwrap();
}
}
struct Builder {
world: PhysicsWorld,
layers: Layers,
frame: Frame,
surface: Surface,
terrain: Vec<BodyId>,
}
impl Builder {
fn add(&mut self, key: SceneKey) {
match key {
SceneKey::Plane { deg10, bits, .. } => {
let tan = degrees(deg10).to_radians().tan();
self.field([0.0; 3], SAMPLES, 1.0, bits.into(), move |x, _| tan * x);
}
SceneKey::Ramp { deg10, .. } => {
let tan = degrees(deg10).to_radians().tan();
self.field([0.0; 3], SAMPLES, 1.0, 16, move |x, _| {
tan * (x.clamp(-6.0, 6.0) + 6.0)
});
}
SceneKey::Ridge { deg10, .. } => {
let tan = degrees(deg10).to_radians().tan();
self.field([0.0; 3], SAMPLES, 1.0, 16, move |x, _| {
tan * (6.0 - x.abs()).max(0.0)
});
}
SceneKey::Step { height_mm, rounded } => {
self.flat();
let h = f64::from(height_mm) / 1000.0;
let bottom = -1.0;
self.block(
[2.0, 0.5 * (h + bottom), 0.0],
[2.0, 0.5 * (h - bottom), 6.0],
0.0,
radius(rounded),
);
}
SceneKey::Ledge { drop_mm, rounded } => {
self.flat();
let h = f64::from(drop_mm) / 1000.0;
let bottom = -1.0;
self.block(
[-2.0, 0.5 * (h + bottom), 0.0],
[2.0, 0.5 * (h - bottom), 6.0],
0.0,
radius(rounded),
);
}
SceneKey::Wall { thick } => {
self.flat();
let half_x = if thick { 1.0 } else { 0.1 };
let face = f64::from(RADIUS) - 0.05;
self.block([face + half_x, 1.0, 0.0], [half_x, 1.0, 2.0], 0.0, 0.0);
}
SceneKey::BoxRamp { deg10, .. } => {
let angle = degrees(deg10).to_radians();
let thickness = 0.5;
let centre = [thickness * angle.sin(), -thickness * angle.cos(), 0.0];
self.block(centre, [12.0, thickness, 9.0], angle as f32, 0.0);
self.field([0.0, -20.0, 0.0], SAMPLES, 1.0, 16, |_, _| 0.0);
}
SceneKey::Flat => self.flat(),
SceneKey::Seam { profile, split } => match split {
Split::Continuous => {
self.field([0.0; 3], 65, 1.0, 16, move |x, _| profile.height(x));
}
Split::Compound => {
self.fields_in_one_body(&[[-16.0, 0.0, 0.0], [16.0, 0.0, 0.0]], move |x, _| {
profile.height(x)
});
}
Split::Pair | Split::Raised => {
let lift = if split == Split::Raised { 0.03 } else { 0.0 };
self.field([-16.0, 0.0, 0.0], SAMPLES, 1.0, 16, move |x, _| {
profile.height(x)
});
self.field([16.0, 0.0, 0.0], SAMPLES, 1.0, 16, move |x, _| {
profile.height(x) + lift
});
}
},
SceneKey::RadialSpiral {
deg10,
spacing_mm,
bits,
} => {
let tan = degrees(deg10).to_radians().tan();
let spacing = f64::from(spacing_mm) / 1000.0;
self.field([0.0; 3], SAMPLES, spacing, bits.into(), move |x, _| {
spiral_height(tan, x)
});
}
SceneKey::RadialCrest { deg10 } => {
let tan = degrees(deg10).to_radians().tan();
self.field([0.0; 3], SAMPLES, 0.5, 16, move |x, _| {
if x >= 0.0 {
(R * R - x * x).sqrt() - R
} else {
spiral_height(tan, x)
}
});
}
SceneKey::RadialStep { height_mm } => {
self.planet_chunk(0.0);
let face = 1.0 + f64::from(RADIUS);
let top = flat_ground(face, 0.0) + f64::from(height_mm) / 1000.0;
self.block([face + 2.0, top - 1.5, 0.0], [2.0, 1.5, 2.0], 0.0, 0.0);
}
SceneKey::RadialSeam => {
self.planet_chunk(0.0);
self.planet_chunk(2.0 * (16.0 / R).asin());
}
SceneKey::RadialWall => {
self.planet_chunk(0.0);
self.block([0.45, 1.0, 0.0], [0.1, 1.0, 2.0], 0.0, 0.0);
}
SceneKey::HighLedgeBesideWall { face_mm, lean_cdeg } => {
self.flat();
self.high_ledge_beside_wall(face_mm, lean_cdeg);
}
SceneKey::RadialHighLedgeBesideWall { face_mm, lean_cdeg } => {
self.planet_chunk(0.0);
self.high_ledge_beside_wall(face_mm, lean_cdeg);
}
}
}
fn high_ledge_beside_wall(&mut self, face_mm: u16, lean_cdeg: i16) {
let top = HIGH_LEDGE_DROP;
self.block([-2.0, top - 1.5, 0.0], [2.0, 1.5, 2.0], 0.0, 0.0);
let face = [
0.0,
top + f64::from(REST_HEIGHT),
f64::from(face_mm) / 1000.0,
];
let half = [4.0, 0.5 * top + 1.5, 0.1];
let upright_centre = [0.0, 0.5 * top + 0.5, face[2] + half[2]];
let lean = (f64::from(lean_cdeg) / 100.0).to_radians() as f32;
let turn = quat_about(Vec3::new(1.0, 0.0, 0.0), lean);
let centre = add(face, rotate(turn, sub(upright_centre, face)));
self.turned_block(centre, half, turn, 0.0);
}
fn flat(&mut self) {
self.field([0.0; 3], SAMPLES, 1.0, 16, |_, _| 0.0);
}
fn field(
&mut self,
centre: V3,
samples: usize,
spacing: f64,
bits: u32,
height: impl Fn(f64, f64) -> f64,
) {
let (shape, field) = self.field_shape(centre, samples, spacing, bits, height);
let body = self
.world
.create_body(
&shape,
&BodySettings::new_static()
.position(rvec3(field.frame.origin))
.rotation(field.frame.rotation)
.object_layer(self.layers.terrain),
)
.unwrap();
self.terrain.push(body);
self.surface.fields.push(field);
}
fn fields_in_one_body(&mut self, centres: &[V3], height: impl Fn(f64, f64) -> f64 + Copy) {
let parts: Vec<(Shape, Field)> = centres
.iter()
.map(|¢re| self.field_shape(centre, SAMPLES, 1.0, 16, height))
.collect();
let children: Vec<CompoundChild<'_>> = parts
.iter()
.zip(centres)
.map(|((shape, _), ¢re)| CompoundChild {
shape,
position: vec3(centre),
rotation: Quat::IDENTITY,
user_data: Groups::FEATURE,
})
.collect();
let compound = Shape::new_compound(&children).unwrap();
let body = self
.world
.create_body(
&compound,
&BodySettings::new_static()
.position(rvec3(self.frame.origin))
.rotation(self.frame.rotation)
.object_layer(self.layers.terrain),
)
.unwrap();
self.terrain.push(body);
self.surface
.fields
.extend(parts.into_iter().map(|(_, field)| field));
}
fn field_shape(
&self,
centre: V3,
samples: usize,
spacing: f64,
bits: u32,
height: impl Fn(f64, f64) -> f64,
) -> (Shape, Field) {
let half = (samples - 1) as f64 / 2.0 * spacing;
let mut heights = Vec::with_capacity(samples * samples);
for iz in 0..samples {
for ix in 0..samples {
let (x, z) = (ix as f64 * spacing - half, iz as f64 * spacing - half);
heights.push(height(centre[0] + x, centre[2] + z));
}
}
let bits = match FIELD_BITS_OVERRIDE.load(Ordering::Relaxed) {
0 => bits,
forced => forced,
};
let offset = [-half, 0.0, -half];
let settings = HeightFieldSettings::default()
.offset(vec3(offset))
.scale(Vec3::new(spacing as f32, 1.0, spacing as f32))
.bits_per_sample(bits);
let samples_f32: Vec<f32> = heights.iter().map(|&h| h as f32).collect();
let shape = Shape::new_height_field(samples as u32, &samples_f32, &settings).unwrap();
let frame = Frame {
origin: self.frame.to_world_point(centre),
rotation: self.frame.rotation,
};
let intended = heights.iter().map(|&h| f64::from(h as f32)).collect();
let field = Field::decode(&shape, frame, samples, spacing, offset, intended, bits);
(shape, field)
}
fn planet_chunk(&mut self, angle: f64) {
let shape = flat_terrain();
let (position, rotation) = chunk_pose(angle);
let body = self
.world
.create_body(
&shape,
&BodySettings::new_static()
.position(position)
.rotation(rotation)
.object_layer(self.layers.terrain),
)
.unwrap();
self.terrain.push(body);
let intended = (0..SAMPLES * SAMPLES)
.map(|i| {
let (x, z) = ((i % SAMPLES) as f64 - 16.0, (i / SAMPLES) as f64 - 16.0);
f64::from(((R * R - x * x - z * z).sqrt() - R) as f32)
})
.collect();
let frame = Frame {
origin: crate::common::math::v3(position),
rotation,
};
self.surface.fields.push(Field::decode(
&shape,
frame,
SAMPLES,
1.0,
[-16.0, 0.0, -16.0],
intended,
16,
));
}
fn block(&mut self, centre: V3, half: V3, tilt_z: f32, convex_radius: f64) {
let tilt = quat_about(Vec3::new(0.0, 0.0, 1.0), tilt_z);
self.turned_block(centre, half, tilt, convex_radius);
}
fn turned_block(&mut self, centre: V3, half: V3, tilt: Quat, convex_radius: f64) {
let shape = Shape::new_box_with_convex_radius(vec3(half), convex_radius as f32).unwrap();
let chunk = Shape::new_compound(&[CompoundChild {
shape: &shape,
position: vec3(centre),
rotation: tilt,
user_data: Groups::STRUCTURE,
}])
.unwrap();
self.world
.create_body(
&chunk,
&BodySettings::new_static()
.position(rvec3(self.frame.origin))
.rotation(self.frame.rotation)
.object_layer(self.layers.chunk),
)
.unwrap();
self.surface.blocks.push(Block {
frame: Frame {
origin: self.frame.to_world_point(centre),
rotation: self.frame.to_world_rotation(tilt),
},
half,
convex_radius,
});
}
fn actor(&mut self) -> BodyId {
add_actor(&mut self.world, &self.layers)
}
}
pub fn add_actor(world: &mut PhysicsWorld, layers: &Layers) -> BodyId {
let capsule = capsule();
let shape = Shape::new_compound(&[CompoundChild {
shape: &capsule,
position: Vec3::new(0.0, CENTRE_UP, 0.0),
rotation: Quat::IDENTITY,
user_data: Groups::ACTOR,
}])
.unwrap();
world
.create_body(
&shape,
&BodySettings::new_kinematic()
.position(RVec3::new(0.0, -500.0, 0.0))
.object_layer(layers.actor),
)
.unwrap()
}
fn radius(rounded: bool) -> f64 {
if rounded {
0.05
} else {
0.0
}
}
pub fn spiral_height(tan: f64, x: f64) -> f64 {
let at = |phi: f64| R * (tan * phi).exp() * phi.sin();
let (mut low, mut high) = (-0.5, 0.5);
for _ in 0..200 {
let mid = 0.5 * (low + high);
if at(mid) < x {
low = mid;
} else {
high = mid;
}
}
let phi = 0.5 * (low + high);
R * (tan * phi).exp() * phi.cos() - R
}
pub fn flat_ground(x: f64, z: f64) -> f64 {
(R * R - x * x - z * z).sqrt() - R
}
pub fn resting_on_planet(x: f64, z: f64) -> V3 {
let ground = [x, flat_ground(x, z), z];
add(ground, scale(up_at(ground), f64::from(REST_HEIGHT)))
}