use std::{ops::RangeInclusive, sync::Arc, time::Duration};
use arc_swap::ArcSwap;
use cgmath::{vec3, Angle, Deg, InnerSpace, Matrix3, Vector3};
use perovskite_core::{
constants::permissions,
coordinates::BlockCoordinate,
protocol::blocks::{
block_type_def::{self, PhysicsInfo},
BlockTypeDef,
},
};
use tracy_client::{plot, span};
use crate::block_renderer::ClientBlockTypeManager;
use super::{
input::{BoundAction, InputState},
settings::GameSettings,
ChunkManagerView, ClientState,
};
const PLAYER_WIDTH: f64 = 0.75;
const EYE_TO_TOP: f64 = 0.2;
const EYE_TO_BTM: f64 = 1.5;
const PLAYER_COLLISIONBOX_CORNER_POS: Vector3<f64> =
vec3(PLAYER_WIDTH / 2., EYE_TO_TOP, PLAYER_WIDTH / 2.);
const PLAYER_COLLISIONBOX_CORNER_NEG: Vector3<f64> =
vec3(-PLAYER_WIDTH / 2., -EYE_TO_BTM, -PLAYER_WIDTH / 2.);
const _PLAYER_COLLISIONBOX_CORNERS: [Vector3<f64>; 8] = [
vec3(PLAYER_WIDTH / 2., EYE_TO_TOP, PLAYER_WIDTH / 2.),
vec3(PLAYER_WIDTH / 2., EYE_TO_TOP, -PLAYER_WIDTH / 2.),
vec3(PLAYER_WIDTH / 2., EYE_TO_BTM, PLAYER_WIDTH / 2.),
vec3(PLAYER_WIDTH / 2., EYE_TO_BTM, -PLAYER_WIDTH / 2.),
vec3(-PLAYER_WIDTH / 2., -EYE_TO_TOP, PLAYER_WIDTH / 2.),
vec3(-PLAYER_WIDTH / 2., -EYE_TO_TOP, -PLAYER_WIDTH / 2.),
vec3(-PLAYER_WIDTH / 2., -EYE_TO_BTM, PLAYER_WIDTH / 2.),
vec3(-PLAYER_WIDTH / 2., -EYE_TO_BTM, -PLAYER_WIDTH / 2.),
];
const TRAVERSABLE_BUMP_HEIGHT_LANDED: f64 = 0.51;
const TRAVERSABLE_BUMP_HEIGHT_MIDAIR: f64 = 0.2;
const FLY_SPEED: f64 = 6.0;
const JUMP_VELOCITY: f64 = 6.0;
const GRAVITY_ACCEL: f64 = 16.;
const TERMINAL_VELOCITY: f64 = 90.;
const DAMPING: f64 = GRAVITY_ACCEL / (TERMINAL_VELOCITY * TERMINAL_VELOCITY);
const COLLISION_EPS: f64 = 0.000005;
const _DETECTION_EPS: f64 = COLLISION_EPS * 2.;
const _FLOAT_EPS: f64 = 0.000001;
trait BumpMagnitude {
fn bias_with(self, bias: f64) -> Self;
}
impl BumpMagnitude for f64 {
fn bias_with(self, bias: f64) -> Self {
self + (bias * self.signum())
}
}
impl BumpMagnitude for Vector3<f64> {
fn bias_with(self, bias: f64) -> Self {
vec3(
self.x.bias_with(bias),
self.y.bias_with(bias),
self.z.bias_with(bias),
)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum PhysicsMode {
Standard,
FlyingCollisions,
Noclip,
}
impl PhysicsMode {
fn next(&self, allow_fly: bool, allow_noclip: bool) -> PhysicsMode {
match self {
PhysicsMode::Standard => {
if allow_fly {
PhysicsMode::FlyingCollisions
} else {
PhysicsMode::Standard
}
}
PhysicsMode::FlyingCollisions => {
if allow_noclip {
PhysicsMode::Noclip
} else {
PhysicsMode::Standard
}
}
PhysicsMode::Noclip => PhysicsMode::Standard,
}
}
}
pub(crate) struct PhysicsState {
pos: cgmath::Vector3<f64>,
az: Deg<f64>,
el: Deg<f64>,
target_az: Deg<f64>,
target_el: Deg<f64>,
physics_mode: PhysicsMode,
y_velocity: f64,
landed_last_frame: bool,
last_land_height: f64,
bump_decay: f64,
settings: Arc<ArcSwap<GameSettings>>,
can_fly: bool,
can_fast: bool,
can_noclip: bool,
}
impl PhysicsState {
pub(crate) fn new(settings: Arc<ArcSwap<GameSettings>>) -> Self {
Self {
pos: cgmath::vec3(0., 0., -4.),
az: Deg(0.),
el: Deg(0.),
target_az: Deg(0.),
target_el: Deg(0.),
physics_mode: PhysicsMode::Standard,
y_velocity: 0.,
landed_last_frame: false,
last_land_height: 0.,
bump_decay: 0.,
settings,
can_fly: false,
can_fast: false,
can_noclip: false,
}
}
pub(crate) fn update_and_get(
&mut self,
client_state: &ClientState,
_aspect_ratio: f64,
delta: Duration,
) -> (cgmath::Vector3<f64>, (f64, f64)) {
let mut input = client_state.input.lock();
if input.take_just_pressed(BoundAction::TogglePhysics) {
self.physics_mode = self.physics_mode.next(self.can_fly, self.can_noclip);
client_state.chat.lock().show_client_message(format!(
"Physics mode changed to {}",
match self.physics_mode {
PhysicsMode::Standard => "standard",
PhysicsMode::FlyingCollisions => "flying",
PhysicsMode::Noclip => "noclip",
}
));
}
let (x, y) = input.take_mouse_delta();
self.target_az = Deg((self.target_az.0 + (x)).rem_euclid(360.0));
self.target_el = Deg((self.target_el.0 - (y)).clamp(-90.0, 90.0));
self.update_smooth_angles(delta);
match self.physics_mode {
PhysicsMode::Standard => self.update_standard(&mut input, delta, client_state),
PhysicsMode::FlyingCollisions => {
self.update_flying(&mut input, delta, true, client_state)
}
PhysicsMode::Noclip => self.update_flying(&mut input, delta, false, client_state),
}
let adjusted_for_bump = vec3(self.pos.x, self.pos.y - self.bump_decay, self.pos.z);
(adjusted_for_bump, self.angle())
}
fn update_standard(
&mut self,
input: &mut InputState,
delta: std::time::Duration,
client_state: &ClientState,
) {
let _span = span!("physics_standard");
let delta_secs = delta.as_secs_f64();
let chunks = client_state.chunks.read_lock();
let block_types = &client_state.block_types;
let surrounding_block = get_block(
BlockCoordinate {
x: self.pos.x.round() as i32,
y: (self.pos.y - EYE_TO_BTM).round() as i32,
z: self.pos.z.round() as i32,
},
&chunks,
block_types,
);
let block_physics = surrounding_block.and_then(|(x, _)| x.physics_info.as_ref());
let (mut new_yv, target) = match block_physics {
Some(PhysicsInfo::Air(_)) => {
self.update_target_air(input, self.pos, delta_secs * 3.0, delta)
}
Some(PhysicsInfo::Fluid(fluid_data)) => {
let surface_test_block = get_block(
BlockCoordinate {
x: self.pos.x.round() as i32,
y: (self.pos.y - EYE_TO_BTM + fluid_data.surface_thickness).round() as i32,
z: self.pos.z.round() as i32,
},
&chunks,
block_types,
);
let is_surface = matches!(
surface_test_block.and_then(|(x, _)| x.physics_info.as_ref()),
Some(PhysicsInfo::Air(_))
);
self.update_target_fluid(input, self.pos, fluid_data, delta, is_surface)
}
Some(PhysicsInfo::Solid(_)) => {
self.update_target_air(input, self.pos, delta_secs * 3.0, delta)
}
Some(PhysicsInfo::SolidCustomCollisionboxes(_)) => {
self.update_target_air(input, self.pos, delta_secs * 3.0, delta)
}
None => self.update_target_air(input, self.pos, delta_secs * 3.0, delta),
};
plot!("physics_delta", (target - self.pos).magnitude());
let new_pos = clamp_collisions_loop(self.pos, target, &chunks, block_types);
if (new_pos.y - target.y) > COLLISION_EPS {
self.landed_last_frame = true;
self.last_land_height = new_pos.y;
new_yv = 0.;
} else if (new_pos.y - target.y) < -COLLISION_EPS {
new_yv = 0.;
self.landed_last_frame = false;
} else {
self.landed_last_frame = false;
}
let bump_height = if self.landed_last_frame {
TRAVERSABLE_BUMP_HEIGHT_LANDED
} else {
(self.last_land_height + TRAVERSABLE_BUMP_HEIGHT_MIDAIR - new_pos.y)
.clamp(0., TRAVERSABLE_BUMP_HEIGHT_MIDAIR)
};
self.pos = self.update_with_bumping(
new_pos,
bump_height,
target,
chunks,
block_types,
delta_secs,
);
self.y_velocity = new_yv;
}
fn update_with_bumping(
&mut self,
mut new_pos: Vector3<f64>,
bump_height: f64,
target: Vector3<f64>,
chunks: ChunkManagerView<'_>,
block_types: &std::sync::Arc<ClientBlockTypeManager>,
delta_secs: f64,
) -> Vector3<f64> {
let pre_bump_y = new_pos.y;
if bump_height > 0.
&& ((new_pos.x - target.x).abs() > COLLISION_EPS
|| (new_pos.z - target.z).abs() > COLLISION_EPS)
{
let bump_target = vec3(new_pos.x, new_pos.y + bump_height, new_pos.z);
let bump_outcome = clamp_collisions_loop(new_pos, bump_target, &chunks, block_types);
let post_bump_target = vec3(target.x, bump_outcome.y, target.z);
let post_bump_outcome =
clamp_collisions_loop(bump_outcome, post_bump_target, &chunks, block_types);
if (post_bump_outcome.x - bump_target.x).abs() > COLLISION_EPS
|| (post_bump_outcome.z - bump_target.z).abs() > COLLISION_EPS
{
let down_bump_target = vec3(
post_bump_outcome.x,
post_bump_outcome.y - bump_height,
post_bump_outcome.z,
);
new_pos = clamp_collisions_loop(
post_bump_outcome,
down_bump_target,
&chunks,
block_types,
);
self.bump_decay = self.bump_decay.max(new_pos.y - pre_bump_y);
if self.settings.load().render.physics_debug {
log::info!("bump success {bump_height} \ntarget : {bump_target:?},\noutcome : {bump_outcome:?}\nptarget : {post_bump_target:?}\npoutcome: {post_bump_outcome:?} \ndtarget : {down_bump_target:?}\nnewpos : {new_pos:?}");
log::info!("bump decay: {}", self.bump_decay);
}
}
}
self.bump_decay = (self.bump_decay - (delta_secs * BUMP_DECAY_SPEED)).max(0.0);
new_pos
}
fn update_target_air(
&mut self,
input: &mut InputState,
pos: Vector3<f64>,
distance: f64,
time_delta: Duration,
) -> (f64, Vector3<f64>) {
let mut target = self.apply_movement_input(pos, input, distance);
let mut new_yv = self.y_velocity - (time_delta.as_secs_f64() * GRAVITY_ACCEL)
+ (DAMPING * self.y_velocity.min(0.).powi(2) * time_delta.as_secs_f64());
if input.is_pressed(BoundAction::Jump) && self.landed_last_frame {
self.landed_last_frame = false;
new_yv = JUMP_VELOCITY;
}
target.y += new_yv * time_delta.as_secs_f64();
(new_yv, target)
}
fn apply_movement_input(
&self,
pos: Vector3<f64>,
input: &mut InputState,
mut distance: f64,
) -> Vector3<f64> {
if self.can_fast && input.is_pressed(BoundAction::FastMove) {
distance *= Self::FAST_MOVE_RATIO;
}
let mut target = pos;
if input.is_pressed(BoundAction::MoveForward) {
target.z += self.az.cos() * distance;
target.x += self.az.sin() * distance;
} else if input.is_pressed(BoundAction::MoveBackward) {
target.z -= self.az.cos() * distance;
target.x -= self.az.sin() * distance;
}
if input.is_pressed(BoundAction::MoveLeft) {
target.z += self.az.sin() * distance;
target.x -= self.az.cos() * distance;
} else if input.is_pressed(BoundAction::MoveRight) {
target.z -= self.az.sin() * distance;
target.x += self.az.cos() * distance;
}
target
}
const FAST_MOVE_RATIO: f64 = 3.0;
fn update_target_fluid(
&self,
input: &mut InputState,
pos: Vector3<f64>,
fluid_data: &perovskite_core::protocol::blocks::FluidPhysicsInfo,
delta: Duration,
is_surface: bool,
) -> (f64, Vector3<f64>) {
let delta = delta.as_secs_f64();
let (horizontal_speed, vertical_velocity, jump_velocity, sink_velocity) = if is_surface {
(
fluid_data.surf_horizontal_speed,
fluid_data.surf_vertical_speed,
fluid_data.surf_jump_speed,
fluid_data.surf_sink_speed,
)
} else {
(
fluid_data.horizontal_speed,
fluid_data.vertical_speed,
fluid_data.jump_speed,
fluid_data.sink_speed,
)
};
let mut target = self.apply_movement_input(pos, input, horizontal_speed * delta);
let vy = if input.is_pressed(BoundAction::Jump) {
jump_velocity
} else if input.is_pressed(BoundAction::Descend) {
sink_velocity
} else {
vertical_velocity
};
target.y += vy * delta;
(vy, target)
}
fn update_flying(
&mut self,
input: &mut InputState,
delta: std::time::Duration,
collisions: bool,
client_state: &ClientState,
) {
let distance = delta.as_secs_f64() * FLY_SPEED * 4.0;
let mut target_pos = self.apply_movement_input(self.pos, input, distance);
if input.is_pressed(BoundAction::Jump) {
target_pos.y += distance;
} else if input.is_pressed(BoundAction::Descend) {
target_pos.y -= distance;
}
if collisions {
let chunks = client_state.chunks.read_lock();
let block_types = &client_state.block_types;
let new_pos = clamp_collisions_loop(self.pos, target_pos, &chunks, block_types);
self.pos = self.update_with_bumping(
new_pos,
TRAVERSABLE_BUMP_HEIGHT_LANDED,
target_pos,
chunks,
block_types,
delta.as_secs_f64(),
);
} else {
self.pos = target_pos
}
}
pub(crate) fn pos(&self) -> Vector3<f64> {
self.pos
}
pub(crate) fn set_position(&mut self, pos: Vector3<f64>) {
self.pos = pos;
}
pub(crate) fn update_permissions(
&mut self,
permissions: &[String],
client_state: &ClientState,
) {
self.can_fly = permissions.iter().any(|p| p == permissions::FLY);
self.can_fast = permissions.iter().any(|p| p == permissions::FAST_MOVE);
self.can_noclip = permissions.iter().any(|p| p == permissions::NOCLIP);
let new_physics_mode = if !self.can_noclip {
match self.physics_mode {
PhysicsMode::Standard => PhysicsMode::Standard,
PhysicsMode::FlyingCollisions => PhysicsMode::FlyingCollisions,
PhysicsMode::Noclip => PhysicsMode::FlyingCollisions,
}
} else if !self.can_fly {
PhysicsMode::Standard
} else {
self.physics_mode
};
if new_physics_mode != self.physics_mode {
client_state.chat.lock().show_client_message(format!(
"Physics mode changed to {}",
match new_physics_mode {
PhysicsMode::Standard => "standard",
PhysicsMode::FlyingCollisions => "flying",
PhysicsMode::Noclip => "noclip",
}
));
}
self.physics_mode = new_physics_mode;
}
pub(crate) fn angle(&self) -> (f64, f64) {
(self.az.0, self.el.0)
}
fn update_smooth_angles(&mut self, delta: std::time::Duration) {
let factor =
ANGLE_SMOOTHING_FACTOR.powf(delta.as_secs_f64() / ANGLE_SMOOTHING_REFERENCE_DELTA);
self.el = (self.el * factor) + (self.target_el * (1.0 - factor));
let az_x = (self.az.cos() * factor) + (self.target_az.cos() * (1.0 - factor));
let az_y = (self.az.sin() * factor) + (self.target_az.sin() * (1.0 - factor));
self.az = Deg::atan2(az_y, az_x);
}
}
const ANGLE_SMOOTHING_FACTOR: f64 = 0.8;
const ANGLE_SMOOTHING_REFERENCE_DELTA: f64 = 1.0 / 165.0;
const BUMP_DECAY_SPEED: f64 = 3.0;
fn clamp_collisions_loop(
old_pos: Vector3<f64>,
target: Vector3<f64>,
chunks: &ChunkManagerView,
block_types: &ClientBlockTypeManager,
) -> Vector3<f64> {
if (target - old_pos).magnitude2() < (COLLISION_EPS * COLLISION_EPS) {
return old_pos;
}
let mut prev = old_pos;
loop {
let intended_delta = target - prev;
let shortened_mag = intended_delta.magnitude().min(0.05);
if shortened_mag < COLLISION_EPS {
return prev;
}
let shortened_delta = (intended_delta / intended_delta.magnitude()) * shortened_mag;
let shortened_target = prev + shortened_delta;
let current = prev;
let current = clamp_collisions(
current,
vec3(shortened_target.x, current.y, current.z),
chunks,
block_types,
);
let current = clamp_collisions(
current,
vec3(current.x, shortened_target.y, current.z),
chunks,
block_types,
);
let current = clamp_collisions(
current,
vec3(current.x, current.y, shortened_target.z),
chunks,
block_types,
);
if (prev - current).magnitude2() < (COLLISION_EPS * COLLISION_EPS) {
return current;
}
prev = current;
}
}
struct CollisionBox {
min: Vector3<f64>,
max: Vector3<f64>,
}
impl CollisionBox {
fn full_cube(coord: Vector3<f64>) -> CollisionBox {
CollisionBox {
min: vec3(coord.x - 0.5, coord.y - 0.5, coord.z - 0.5),
max: vec3(coord.x + 0.5, coord.y + 0.5, coord.z + 0.5),
}
}
fn from_aabb(
coord: Vector3<f64>,
aa_box: &perovskite_core::protocol::blocks::AxisAlignedBox,
variant: u16,
) -> CollisionBox {
let (min, max) = apply_aabox_transformation(aa_box, coord, variant);
CollisionBox { min, max }
}
}
pub(crate) fn apply_aabox_transformation(
aa_box: &perovskite_core::protocol::blocks::AxisAlignedBox,
coord: Vector3<f64>,
variant: u16,
) -> (Vector3<f64>, Vector3<f64>) {
let min = vec3(
aa_box.x_min as f64,
aa_box.y_min as f64,
aa_box.z_min as f64,
);
let max = vec3(
aa_box.x_max as f64,
aa_box.y_max as f64,
aa_box.z_max as f64,
);
let (min, max) = match aa_box.rotation() {
perovskite_core::protocol::blocks::AxisAlignedBoxRotation::None => {
(coord + min, coord + max)
}
perovskite_core::protocol::blocks::AxisAlignedBoxRotation::Nesw => {
let r_matrix = Matrix3::from_angle_y(Deg(90.0 * (variant % 4) as f64));
let v1 = coord + r_matrix * min;
let v2 = coord + r_matrix * max;
let min = vec3(v1.x.min(v2.x), v1.y.min(v2.y), v1.z.min(v2.z));
let max = vec3(v1.x.max(v2.x), v1.y.max(v2.y), v1.z.max(v2.z));
(min, max)
}
};
(min, max)
}
fn clamp_collisions(
old_pos: Vector3<f64>,
mut new_pos: Vector3<f64>,
chunks: &ChunkManagerView,
block_types: &ClientBlockTypeManager,
) -> Vector3<f64> {
let collision_boxes = get_collision_boxes(new_pos, chunks, block_types);
let player_bbox_min = vec3(
PLAYER_COLLISIONBOX_CORNER_NEG.x + new_pos.x,
PLAYER_COLLISIONBOX_CORNER_NEG.y + new_pos.y,
PLAYER_COLLISIONBOX_CORNER_NEG.z + new_pos.z,
);
let player_bbox_max = vec3(
PLAYER_COLLISIONBOX_CORNER_POS.x + new_pos.x,
PLAYER_COLLISIONBOX_CORNER_POS.y + new_pos.y,
PLAYER_COLLISIONBOX_CORNER_POS.z + new_pos.z,
);
#[inline]
fn overlaps(min1: f64, max1: f64, min2: f64, max2: f64) -> bool {
min1 <= max2 && min2 <= max1
}
for cbox in collision_boxes {
if overlaps(cbox.min.x, cbox.max.x, player_bbox_min.x, player_bbox_max.x)
&& overlaps(cbox.min.y, cbox.max.y, player_bbox_min.y, player_bbox_max.y)
&& overlaps(cbox.min.z, cbox.max.z, player_bbox_min.z, player_bbox_max.z)
{
new_pos = clamp_single_block(old_pos, new_pos, cbox);
}
}
new_pos
}
#[inline]
fn clamp_single_block(old: Vector3<f64>, new: Vector3<f64>, cbox: CollisionBox) -> Vector3<f64> {
vec3(
clamp_single_axis(
old.x,
new.x,
cbox.min.x,
cbox.max.x,
PLAYER_COLLISIONBOX_CORNER_POS.x,
PLAYER_COLLISIONBOX_CORNER_NEG.x,
),
clamp_single_axis(
old.y,
new.y,
cbox.min.y,
cbox.max.y,
PLAYER_COLLISIONBOX_CORNER_POS.y,
PLAYER_COLLISIONBOX_CORNER_NEG.y,
),
clamp_single_axis(
old.z,
new.z,
cbox.min.z,
cbox.max.z,
PLAYER_COLLISIONBOX_CORNER_POS.z,
PLAYER_COLLISIONBOX_CORNER_NEG.z,
),
)
}
fn clamp_single_axis(
old: f64,
new: f64,
obstacle_min: f64,
obstacle_max: f64,
pos_bias: f64,
neg_bias: f64,
) -> f64 {
debug_assert!(pos_bias > 0.);
debug_assert!(neg_bias < 0.);
match new.total_cmp(&old) {
std::cmp::Ordering::Less => {
let boundary = obstacle_max - neg_bias;
if old + COLLISION_EPS > boundary && new - COLLISION_EPS < boundary {
boundary + COLLISION_EPS
} else {
new
}
}
std::cmp::Ordering::Equal => new,
std::cmp::Ordering::Greater => {
let boundary = obstacle_min - pos_bias;
if old - COLLISION_EPS < boundary && new + COLLISION_EPS > boundary {
boundary - COLLISION_EPS
} else {
new
}
}
}
}
fn get_block<'a>(
coord: BlockCoordinate,
chunks: &ChunkManagerView,
block_types: &'a ClientBlockTypeManager,
) -> Option<(&'a BlockTypeDef, u16)> {
let chunk = chunks.get(&coord.chunk())?;
let id = chunk.get_single(coord.offset());
let block = block_types.get_blockdef(id)?;
Some((block, id.variant()))
}
#[inline]
fn inclusive<T: Ord>(a: T, b: T) -> RangeInclusive<T> {
if b >= a {
a..=b
} else {
b..=a
}
}
fn get_collision_boxes(
pos: Vector3<f64>,
chunks: &ChunkManagerView,
block_types: &ClientBlockTypeManager,
) -> Vec<CollisionBox> {
let corner1 = pos + PLAYER_COLLISIONBOX_CORNER_POS;
let corner2 = pos + PLAYER_COLLISIONBOX_CORNER_NEG;
let mut output = vec![];
for x in inclusive(corner1.x.round() as i32, corner2.x.round() as i32) {
for y in inclusive(corner1.y.round() as i32, corner2.y.round() as i32) {
for z in inclusive(corner1.z.round() as i32, corner2.z.round() as i32) {
let coord = BlockCoordinate { x, y, z };
match get_block(coord, chunks, block_types) {
Some((block, variant)) => {
push_collision_boxes(coord.into(), block, variant, &mut output);
}
None => output.push(CollisionBox::full_cube(coord.into())),
}
}
}
}
output
}
fn push_collision_boxes(
coord: Vector3<f64>,
block: &BlockTypeDef,
variant: u16,
output: &mut Vec<CollisionBox>,
) {
match &block.physics_info {
Some(block_type_def::PhysicsInfo::Solid(_)) => output.push(CollisionBox::full_cube(coord)),
Some(block_type_def::PhysicsInfo::Fluid(_)) => {}
Some(block_type_def::PhysicsInfo::Air(_)) => {}
Some(block_type_def::PhysicsInfo::SolidCustomCollisionboxes(boxes)) => {
for box_ in &boxes.boxes {
if box_.variant_mask == 0 || (variant & box_.variant_mask as u16) != 0 {
output.push(CollisionBox::from_aabb(coord, box_, variant));
}
}
}
None => {}
}
}