1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00
azalea/azalea-physics/src/clip.rs
mat 0d16f01571
Fluid physics (#199)
* start implementing fluid physics

* Initial implementation of fluid pushing

* different travel function in water

* bubble columns

* jumping in water

* cleanup

* change ultrawarm to be required

* fix for clippy
2025-01-10 16:45:27 -06:00

388 lines
12 KiB
Rust

use std::collections::HashSet;
use azalea_block::{
fluid_state::{FluidKind, FluidState},
BlockState,
};
use azalea_core::{
aabb::AABB,
block_hit_result::BlockHitResult,
direction::{Axis, Direction},
math::{self, lerp, EPSILON},
position::{BlockPos, Vec3},
};
use azalea_inventory::ItemStack;
use azalea_world::ChunkStorage;
use bevy_ecs::entity::Entity;
use crate::collision::{BlockWithShape, VoxelShape, EMPTY_SHAPE};
#[derive(Debug, Clone)]
pub struct ClipContext {
pub from: Vec3,
pub to: Vec3,
pub block_shape_type: BlockShapeType,
pub fluid_pick_type: FluidPickType,
// pub collision_context: EntityCollisionContext,
}
impl ClipContext {
/// Get the shape of given block, using the type of shape set in
/// [`Self::block_shape_type`].
pub fn block_shape(&self, block_state: BlockState) -> &VoxelShape {
// minecraft passes in the world and blockpos to this function but it's not
// actually necessary. it is for fluid_shape though
match self.block_shape_type {
BlockShapeType::Collider => block_state.collision_shape(),
BlockShapeType::Outline => block_state.outline_shape(),
BlockShapeType::Visual => block_state.collision_shape(),
BlockShapeType::FallDamageResetting => {
if azalea_registry::tags::blocks::FALL_DAMAGE_RESETTING
.contains(&azalea_registry::Block::from(block_state))
{
block_state.collision_shape()
} else {
&EMPTY_SHAPE
}
}
}
}
pub fn fluid_shape(
&self,
fluid_state: FluidState,
world: &ChunkStorage,
pos: &BlockPos,
) -> &VoxelShape {
if self.fluid_pick_type.can_pick(&fluid_state) {
crate::collision::fluid_shape(&fluid_state, world, pos)
} else {
&EMPTY_SHAPE
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum BlockShapeType {
/// The shape that's used for collision.
Collider,
/// The block outline that renders when your cursor is over a block.
Outline,
/// Used by entities when considering their line of sight.
///
/// TODO: visual block shape isn't implemented (it'll just return the
/// collider shape), that's correct for most blocks though
Visual,
FallDamageResetting,
}
#[derive(Debug, Copy, Clone)]
pub enum FluidPickType {
None,
SourceOnly,
Any,
Water,
}
impl FluidPickType {
pub fn can_pick(&self, fluid_state: &FluidState) -> bool {
match self {
Self::None => false,
Self::SourceOnly => fluid_state.amount == 8,
Self::Any => fluid_state.kind != FluidKind::Empty,
Self::Water => fluid_state.kind == FluidKind::Water,
}
}
}
#[derive(Debug, Clone)]
pub struct EntityCollisionContext {
pub descending: bool,
pub entity_bottom: f64,
pub held_item: ItemStack,
// pub can_stand_on_fluid: Box<dyn Fn(&FluidState) -> bool>,
pub entity: Entity,
}
pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResult {
traverse_blocks(
context.from,
context.to,
context,
|ctx, block_pos| {
let block_state = chunk_storage.get_block_state(block_pos).unwrap_or_default();
let fluid_state = FluidState::from(block_state);
let block_shape = ctx.block_shape(block_state);
let interaction_clip = clip_with_interaction_override(
&ctx.from,
&ctx.to,
block_pos,
block_shape,
&block_state,
);
let fluid_shape = ctx.fluid_shape(fluid_state, chunk_storage, block_pos);
let fluid_clip = fluid_shape.clip(&ctx.from, &ctx.to, block_pos);
let distance_to_interaction = interaction_clip
.map(|hit| ctx.from.distance_squared_to(&hit.location))
.unwrap_or(f64::MAX);
let distance_to_fluid = fluid_clip
.map(|hit| ctx.from.distance_squared_to(&hit.location))
.unwrap_or(f64::MAX);
if distance_to_interaction <= distance_to_fluid {
interaction_clip
} else {
fluid_clip
}
},
|context| {
let vec = context.from - context.to;
BlockHitResult::miss(
context.to,
Direction::nearest(vec),
BlockPos::from(context.to),
)
},
)
}
fn clip_with_interaction_override(
from: &Vec3,
to: &Vec3,
block_pos: &BlockPos,
block_shape: &VoxelShape,
_block_state: &BlockState,
) -> Option<BlockHitResult> {
let block_hit_result = block_shape.clip(from, to, block_pos);
if let Some(block_hit_result) = block_hit_result {
// TODO: minecraft calls .getInteractionShape here
// getInteractionShape is empty for almost every shape except cauldons,
// compostors, hoppers, and scaffolding.
let interaction_shape = &*EMPTY_SHAPE;
let interaction_hit_result = interaction_shape.clip(from, to, block_pos);
if let Some(interaction_hit_result) = interaction_hit_result {
if interaction_hit_result.location.distance_squared_to(from)
< block_hit_result.location.distance_squared_to(from)
{
return Some(block_hit_result.with_direction(interaction_hit_result.direction));
}
}
Some(block_hit_result)
} else {
None
}
}
pub fn traverse_blocks<C, T>(
from: Vec3,
to: Vec3,
context: C,
get_hit_result: impl Fn(&C, &BlockPos) -> Option<T>,
get_miss_result: impl Fn(&C) -> T,
) -> T {
if from == to {
return get_miss_result(&context);
}
let right_after_end = Vec3 {
x: lerp(-EPSILON, to.x, from.x),
y: lerp(-EPSILON, to.y, from.y),
z: lerp(-EPSILON, to.z, from.z),
};
let right_before_start = Vec3 {
x: lerp(-EPSILON, from.x, to.x),
y: lerp(-EPSILON, from.y, to.y),
z: lerp(-EPSILON, from.z, to.z),
};
let mut current_block = BlockPos::from(right_before_start);
if let Some(data) = get_hit_result(&context, &current_block) {
return data;
}
let vec = right_after_end - right_before_start;
let vec_sign = Vec3 {
x: math::sign(vec.x),
y: math::sign(vec.y),
z: math::sign(vec.z),
};
#[rustfmt::skip]
let percentage_step = Vec3 {
x: if vec_sign.x == 0. { f64::MAX } else { vec_sign.x / vec.x },
y: if vec_sign.y == 0. { f64::MAX } else { vec_sign.y / vec.y },
z: if vec_sign.z == 0. { f64::MAX } else { vec_sign.z / vec.z },
};
let mut percentage = Vec3 {
x: percentage_step.x
* if vec_sign.x > 0. {
1. - math::fract(right_before_start.x)
} else {
math::fract(right_before_start.x)
},
y: percentage_step.y
* if vec_sign.y > 0. {
1. - math::fract(right_before_start.y)
} else {
math::fract(right_before_start.y)
},
z: percentage_step.z
* if vec_sign.z > 0. {
1. - math::fract(right_before_start.z)
} else {
math::fract(right_before_start.z)
},
};
loop {
if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. {
return get_miss_result(&context);
}
if percentage.x < percentage.y {
if percentage.x < percentage.z {
current_block.x += vec_sign.x as i32;
percentage.x += percentage_step.x;
} else {
current_block.z += vec_sign.z as i32;
percentage.z += percentage_step.z;
}
} else if percentage.y < percentage.z {
current_block.y += vec_sign.y as i32;
percentage.y += percentage_step.y;
} else {
current_block.z += vec_sign.z as i32;
percentage.z += percentage_step.z;
}
if let Some(data) = get_hit_result(&context, &current_block) {
return data;
}
}
}
pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> {
let delta = to - from;
let traversed_blocks = BlockPos::between_closed_aabb(aabb);
if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 {
return traversed_blocks.into_iter().collect();
}
let mut traversed_and_collided_blocks = HashSet::new();
let target_min_pos = aabb.min;
let from_min_pos = target_min_pos - delta;
add_collisions_along_travel(
&mut traversed_and_collided_blocks,
from_min_pos,
target_min_pos,
*aabb,
);
traversed_and_collided_blocks.extend(traversed_blocks);
traversed_and_collided_blocks
}
pub fn add_collisions_along_travel(
collisions: &mut HashSet<BlockPos>,
from: Vec3,
to: Vec3,
aabb: AABB,
) {
let delta = to - from;
let mut min_x = from.x.floor() as i32;
let mut min_y = from.y.floor() as i32;
let mut min_z = from.z.floor() as i32;
let direction_x = math::sign_as_int(delta.x);
let direction_y = math::sign_as_int(delta.y);
let direction_z = math::sign_as_int(delta.z);
let step_x = if direction_x == 0 {
f64::MAX
} else {
direction_x as f64 / delta.x
};
let step_y = if direction_y == 0 {
f64::MAX
} else {
direction_y as f64 / delta.y
};
let step_z = if direction_z == 0 {
f64::MAX
} else {
direction_z as f64 / delta.z
};
let mut cur_x = step_x
* if direction_x > 0 {
1. - math::fract(from.x)
} else {
math::fract(from.x)
};
let mut cur_y = step_y
* if direction_y > 0 {
1. - math::fract(from.y)
} else {
math::fract(from.y)
};
let mut cur_z = step_z
* if direction_z > 0 {
1. - math::fract(from.z)
} else {
math::fract(from.z)
};
let mut step_count = 0;
while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. {
if cur_x < cur_y {
if cur_x < cur_z {
min_x += direction_x;
cur_x += step_x;
} else {
min_z += direction_z;
cur_z += step_z;
}
} else if cur_y < cur_z {
min_y += direction_y;
cur_y += step_y;
} else {
min_z += direction_z;
cur_z += step_z;
}
if step_count > 16 {
break;
}
step_count += 1;
let Some(clip_location) = AABB::clip_with_from_and_to(
&Vec3::new(min_x as f64, min_y as f64, min_z as f64),
&Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64),
&from,
&to,
) else {
continue;
};
let initial_max_x = clip_location
.x
.clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5);
let initial_max_y = clip_location
.y
.clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5);
let initial_max_z = clip_location
.z
.clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5);
let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32;
let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32;
let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32;
for x in min_x..=max_x {
for y in min_y..=max_y {
for z in min_z..=max_z {
collisions.insert(BlockPos::new(x, y, z));
}
}
}
}
}