EG/plugins/user/fluid_dynamics/components/fluid_obstacle.py
2025-10-30 11:46:41 +08:00

631 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
流体障碍物组件
定义流体模拟中的静态或动态障碍物,支持多种形状和复杂的碰撞检测
"""
from panda3d.core import Vec3, Point3
import math
class FluidObstacle:
"""
流体障碍物类
定义流体模拟中的静态或动态障碍物,支持复杂的碰撞检测和响应
"""
# 障碍物类型
TYPE_BOX = "box"
TYPE_SPHERE = "sphere"
TYPE_CYLINDER = "cylinder"
TYPE_CONE = "cone"
TYPE_MESH = "mesh" # 三角网格
def __init__(self, position, size, obstacle_type=TYPE_BOX, mesh_data=None):
"""
初始化流体障碍物
Args:
position (Point3): 障碍物位置
size (Vec3): 障碍物尺寸
obstacle_type (str): 障碍物类型
mesh_data (dict): 网格数据(仅用于网格类型障碍物)
"""
self.position = position
self.size = size
self.obstacle_type = obstacle_type
self.mesh_data = mesh_data # 用于网格类型障碍物
# 障碍物属性
self.is_dynamic = False # 是否为动态障碍物
self.velocity = Vec3(0, 0, 0) # 动态障碍物速度
self.angular_velocity = Vec3(0, 0, 0) # 角速度
self.rotation = Vec3(0, 0, 0) # 旋转角度
# 物理属性
self.friction = 0.5 # 摩擦系数
self.restitution = 0.3 # 恢复系数
self.density = 1000.0 # 密度
self.mass = 1.0 # 质量
# 碰撞属性
self.collision_margin = 0.05 # 碰撞边缘
self.is_trigger = False # 是否为触发器(不产生物理反应)
# 阻力系数
self.drag_coefficient = 0.47 # 阻力系数(球体)
print(f"流体障碍物创建完成: {position}, 类型: {obstacle_type}")
def update(self, dt):
"""
更新障碍物状态
Args:
dt (float): 时间步长
"""
if self.is_dynamic:
# 更新动态障碍物位置
self.position += self.velocity * dt
# 更新旋转
self.rotation += self.angular_velocity * dt
# 应用重力(如果需要)
# self.velocity += Vec3(0, 0, -9.81) * dt
# 应用阻尼
self.velocity *= 0.99
self.angular_velocity *= 0.99
def handle_fluid_interaction(self, particle, dt):
"""
处理与流体粒子的交互
Args:
particle: 流体粒子对象
dt (float): 时间步长
"""
# 如果是触发器,只检测不产生物理反应
if self.is_trigger:
return self._check_collision(particle)
# 检查粒子是否与障碍物相交
collision_info = self._check_collision_detailed(particle)
if collision_info['is_colliding']:
# 计算碰撞响应
self._resolve_collision_advanced(particle, collision_info)
# 应用摩擦力
particle['velocity'] *= (1.0 - self.friction * dt)
# 应用阻力
self._apply_drag_force(particle, dt)
return collision_info['is_colliding']
def _check_collision_detailed(self, particle):
"""
详细的碰撞检测,返回碰撞信息
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
if self.obstacle_type == self.TYPE_BOX:
return self._check_box_collision(particle)
elif self.obstacle_type == self.TYPE_SPHERE:
return self._check_sphere_collision(particle)
elif self.obstacle_type == self.TYPE_CYLINDER:
return self._check_cylinder_collision(particle)
elif self.obstacle_type == self.TYPE_CONE:
return self._check_cone_collision(particle)
elif self.obstacle_type == self.TYPE_MESH and self.mesh_data:
return self._check_mesh_collision(particle)
return collision_info
def _check_box_collision(self, particle):
"""
盒状碰撞检测
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
pos = particle['position']
half_size = self.size * 0.5
min_bound = self.position - half_size
max_bound = self.position + half_size
# 检查是否在盒子内
if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and
min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and
min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin):
collision_info['is_colliding'] = True
# 计算法向量和穿透深度
# 找到最近的面
dx_min = abs(pos.getX() - min_bound.getX())
dx_max = abs(pos.getX() - max_bound.getX())
dy_min = abs(pos.getY() - min_bound.getY())
dy_max = abs(pos.getY() - max_bound.getY())
dz_min = abs(pos.getZ() - min_bound.getZ())
dz_max = abs(pos.getZ() - max_bound.getZ())
min_dist = min(dx_min, dx_max, dy_min, dy_max, dz_min, dz_max)
if min_dist == dx_min:
collision_info['normal'] = Vec3(-1, 0, 0)
collision_info['penetration_depth'] = dx_min
collision_info['contact_point'] = Point3(min_bound.getX(), pos.getY(), pos.getZ())
elif min_dist == dx_max:
collision_info['normal'] = Vec3(1, 0, 0)
collision_info['penetration_depth'] = dx_max
collision_info['contact_point'] = Point3(max_bound.getX(), pos.getY(), pos.getZ())
elif min_dist == dy_min:
collision_info['normal'] = Vec3(0, -1, 0)
collision_info['penetration_depth'] = dy_min
collision_info['contact_point'] = Point3(pos.getX(), min_bound.getY(), pos.getZ())
elif min_dist == dy_max:
collision_info['normal'] = Vec3(0, 1, 0)
collision_info['penetration_depth'] = dy_max
collision_info['contact_point'] = Point3(pos.getX(), max_bound.getY(), pos.getZ())
elif min_dist == dz_min:
collision_info['normal'] = Vec3(0, 0, -1)
collision_info['penetration_depth'] = dz_min
collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_bound.getZ())
else: # dz_max
collision_info['normal'] = Vec3(0, 0, 1)
collision_info['penetration_depth'] = dz_max
collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_bound.getZ())
return collision_info
def _check_sphere_collision(self, particle):
"""
球状碰撞检测
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
pos = particle['position']
radius = self.size.getX() * 0.5
center = self.position
# 计算粒子到球心的距离
distance_vec = pos - center
distance = distance_vec.length()
# 检查是否碰撞
if distance <= radius + self.collision_margin:
collision_info['is_colliding'] = True
# 计算法向量
if distance > 0:
collision_info['normal'] = distance_vec.normalized()
else:
# 如果粒子在球心,使用默认法向量
collision_info['normal'] = Vec3(0, 0, 1)
# 计算穿透深度
collision_info['penetration_depth'] = radius + self.collision_margin - distance
# 计算接触点
collision_info['contact_point'] = center + collision_info['normal'] * radius
return collision_info
def _check_cylinder_collision(self, particle):
"""
圆柱碰撞检测
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
pos = particle['position']
radius = self.size.getX() * 0.5
height = self.size.getZ()
center = self.position
# 计算在XY平面上到圆柱轴的距离
dx = pos.getX() - center.getX()
dy = pos.getY() - center.getY()
distance_xy = math.sqrt(dx*dx + dy*dy)
# 检查Z轴范围
min_z = center.getZ() - height/2.0
max_z = center.getZ() + height/2.0
in_z_range = min_z - self.collision_margin <= pos.getZ() <= max_z + self.collision_margin
# 检查是否在圆柱范围内
if distance_xy <= radius + self.collision_margin and in_z_range:
collision_info['is_colliding'] = True
# 计算法向量
if distance_xy > 0:
# 径向法向量
radial_normal = Vec3(dx, dy, 0).normalized()
# 检查是径向碰撞还是轴向碰撞
if pos.getZ() < min_z or pos.getZ() > max_z:
# 轴向碰撞
if pos.getZ() < min_z:
collision_info['normal'] = Vec3(0, 0, -1)
collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_z)
else:
collision_info['normal'] = Vec3(0, 0, 1)
collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_z)
collision_info['penetration_depth'] = min(abs(pos.getZ() - min_z), abs(pos.getZ() - max_z))
else:
# 径向碰撞
collision_info['normal'] = radial_normal
collision_info['contact_point'] = Point3(
center.getX() + radial_normal.getX() * radius,
center.getY() + radial_normal.getY() * radius,
pos.getZ()
)
collision_info['penetration_depth'] = radius + self.collision_margin - distance_xy
else:
# 粒子在圆柱轴上,使用默认法向量
collision_info['normal'] = Vec3(1, 0, 0)
collision_info['contact_point'] = Point3(center.getX() + radius, center.getY(), pos.getZ())
collision_info['penetration_depth'] = radius + self.collision_margin
return collision_info
def _check_cone_collision(self, particle):
"""
圆锥碰撞检测
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
pos = particle['position']
radius = self.size.getX() * 0.5
height = self.size.getZ()
center = self.position
# 相对于圆锥底部的位置
rel_pos = pos - center
rel_pos.setZ(rel_pos.getZ() + height/2.0) # 调整坐标系使底部在z=0
# 计算在XY平面上到圆锥轴的距离
distance_xy = math.sqrt(rel_pos.getX()**2 + rel_pos.getY()**2)
# 检查是否在圆锥高度范围内
if 0 <= rel_pos.getZ() <= height + self.collision_margin:
# 计算圆锥在当前高度的半径
cone_radius_at_height = (rel_pos.getZ() / height) * radius
# 检查是否在圆锥内部
if distance_xy <= cone_radius_at_height + self.collision_margin:
collision_info['is_colliding'] = True
# 计算法向量和接触点
if rel_pos.getZ() <= self.collision_margin:
# 底面碰撞
collision_info['normal'] = Vec3(0, 0, -1)
collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), center.getZ() - height/2.0)
collision_info['penetration_depth'] = self.collision_margin - rel_pos.getZ()
elif distance_xy <= self.collision_margin:
# 中心轴附近,特殊处理
collision_info['normal'] = Vec3(0, 0, 1)
collision_info['contact_point'] = Point3(center.getX(), center.getY(), pos.getZ())
collision_info['penetration_depth'] = self.collision_margin
else:
# 侧面碰撞
# 计算圆锥表面的法向量
radial_dir = Vec3(rel_pos.getX(), rel_pos.getY(), 0).normalized()
surface_normal = Vec3(
radial_dir.getX(),
radial_dir.getY(),
-radius / height # 圆锥的斜率
).normalized()
collision_info['normal'] = surface_normal
# 计算接触点
contact_radius = cone_radius_at_height
collision_info['contact_point'] = Point3(
center.getX() + radial_dir.getX() * contact_radius,
center.getY() + radial_dir.getY() * contact_radius,
pos.getZ()
)
collision_info['penetration_depth'] = cone_radius_at_height + self.collision_margin - distance_xy
return collision_info
def _check_mesh_collision(self, particle):
"""
网格碰撞检测(简化实现)
Args:
particle: 流体粒子对象
Returns:
dict: 碰撞信息
"""
collision_info = {
'is_colliding': False,
'normal': Vec3(0, 0, 0),
'penetration_depth': 0.0,
'contact_point': Point3(0, 0, 0)
}
# 对于网格碰撞检测,需要使用更复杂的算法
# 这里使用简化的包围盒检测
if self.mesh_data and 'vertices' in self.mesh_data:
vertices = self.mesh_data['vertices']
if vertices:
# 计算网格的包围盒
min_bound = Point3(float('inf'), float('inf'), float('inf'))
max_bound = Point3(float('-inf'), float('-inf'), float('-inf'))
for vertex in vertices:
min_bound.setX(min(min_bound.getX(), vertex.getX()))
min_bound.setY(min(min_bound.getY(), vertex.getY()))
min_bound.setZ(min(min_bound.getZ(), vertex.getZ()))
max_bound.setX(max(max_bound.getX(), vertex.getX()))
max_bound.setY(max(max_bound.getY(), vertex.getY()))
max_bound.setZ(max(max_bound.getZ(), vertex.getZ()))
# 应用位置偏移
min_bound += self.position
max_bound += self.position
pos = particle['position']
# 简单的包围盒检测
if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and
min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and
min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin):
collision_info['is_colliding'] = True
# 简化的法向量计算
collision_info['normal'] = Vec3(0, 0, 1)
collision_info['contact_point'] = pos
collision_info['penetration_depth'] = 0.1
return collision_info
def _resolve_collision_advanced(self, particle, collision_info):
"""
高级碰撞响应
Args:
particle: 流体粒子对象
collision_info (dict): 碰撞信息
"""
if not collision_info['is_colliding']:
return
# 获取碰撞信息
normal = collision_info['normal']
penetration_depth = collision_info['penetration_depth']
contact_point = collision_info['contact_point']
# 调整粒子位置以解决穿透
particle['position'] += normal * penetration_depth
# 计算碰撞响应
velocity = particle['velocity']
velocity_normal = normal * velocity.dot(normal)
velocity_tangent = velocity - velocity_normal
# 应用恢复系数
new_velocity_normal = -velocity_normal * self.restitution
new_velocity_tangent = velocity_tangent * (1.0 - self.friction)
# 更新粒子速度
particle['velocity'] = new_velocity_normal + new_velocity_tangent
# 如果是动态障碍物,也要更新障碍物的速度
if self.is_dynamic:
# 简化的动量交换
impulse = (velocity_normal - new_velocity_normal) * particle.get('mass', 1.0)
self.velocity -= impulse / self.mass
def _apply_drag_force(self, particle, dt):
"""
应用阻力
Args:
particle: 流体粒子对象
dt (float): 时间步长
"""
# 计算相对速度
relative_velocity = particle['velocity'] - self.velocity
speed = relative_velocity.length()
if speed > 0:
# 计算阻力大小
# F = 0.5 * ρ * v² * Cd * A
fluid_density = 1000.0 # 水的密度
cross_sectional_area = 0.01 # 简化的横截面积
drag_force_magnitude = 0.5 * fluid_density * speed * speed * self.drag_coefficient * cross_sectional_area
# 计算阻力方向(与速度方向相反)
drag_direction = -relative_velocity.normalized()
# 应用阻力到粒子
drag_force = drag_direction * drag_force_magnitude
particle['velocity'] += drag_force * dt / particle.get('mass', 1.0)
def set_dynamic(self, is_dynamic, velocity=None, angular_velocity=None):
"""
设置障碍物是否为动态
Args:
is_dynamic (bool): 是否为动态
velocity (Vec3): 线速度
angular_velocity (Vec3): 角速度
"""
self.is_dynamic = is_dynamic
if velocity:
self.velocity = velocity
if angular_velocity:
self.angular_velocity = angular_velocity
def set_physics_properties(self, friction=None, restitution=None, density=None, mass=None):
"""
设置物理属性
Args:
friction (float): 摩擦系数
restitution (float): 恢复系数
density (float): 密度
mass (float): 质量
"""
if friction is not None:
self.friction = max(0.0, min(1.0, friction))
if restitution is not None:
self.restitution = max(0.0, min(1.0, restitution))
if density is not None:
self.density = density
if mass is not None:
self.mass = mass
def set_collision_properties(self, margin=None, is_trigger=None):
"""
设置碰撞属性
Args:
margin (float): 碰撞边缘
is_trigger (bool): 是否为触发器
"""
if margin is not None:
self.collision_margin = max(0.0, margin)
if is_trigger is not None:
self.is_trigger = is_trigger
def apply_force(self, force, dt):
"""
对动态障碍物施加力
Args:
force (Vec3): 力向量
dt (float): 时间步长
"""
if self.is_dynamic:
# F = ma => a = F/m
acceleration = force / self.mass
self.velocity += acceleration * dt
def apply_torque(self, torque, dt):
"""
对动态障碍物施加扭矩
Args:
torque (Vec3): 扭矩向量
dt (float): 时间步长
"""
if self.is_dynamic:
# 简化的扭矩应用
self.angular_velocity += torque * dt / self.mass
def get_bounding_box(self):
"""
获取障碍物的包围盒
Returns:
tuple: (min_bound, max_bound)
"""
if self.obstacle_type == self.TYPE_BOX:
half_size = self.size * 0.5
min_bound = self.position - half_size
max_bound = self.position + half_size
return (min_bound, max_bound)
elif self.obstacle_type == self.TYPE_SPHERE:
radius = self.size.getX() * 0.5
min_bound = self.position - Vec3(radius, radius, radius)
max_bound = self.position + Vec3(radius, radius, radius)
return (min_bound, max_bound)
elif self.obstacle_type == self.TYPE_CYLINDER:
radius = self.size.getX() * 0.5
height = self.size.getZ() * 0.5
min_bound = self.position - Vec3(radius, radius, height)
max_bound = self.position + Vec3(radius, radius, height)
return (min_bound, max_bound)
elif self.obstacle_type == self.TYPE_CONE:
radius = self.size.getX() * 0.5
height = self.size.getZ() * 0.5
min_bound = self.position - Vec3(radius, radius, height)
max_bound = self.position + Vec3(radius, radius, height)
return (min_bound, max_bound)
else:
# 默认包围盒
half_size = self.size * 0.5
min_bound = self.position - half_size
max_bound = self.position + half_size
return (min_bound, max_bound)
def get_volume(self):
"""
计算障碍物体积
Returns:
float: 体积
"""
if self.obstacle_type == self.TYPE_BOX:
return self.size.getX() * self.size.getY() * self.size.getZ()
elif self.obstacle_type == self.TYPE_SPHERE:
radius = self.size.getX() * 0.5
return (4.0/3.0) * math.pi * (radius ** 3)
elif self.obstacle_type == self.TYPE_CYLINDER:
radius = self.size.getX() * 0.5
height = self.size.getZ()
return math.pi * (radius ** 2) * height
elif self.obstacle_type == self.TYPE_CONE:
radius = self.size.getX() * 0.5
height = self.size.getZ()
return (1.0/3.0) * math.pi * (radius ** 2) * height
else:
# 默认按盒状计算
return self.size.getX() * self.size.getY() * self.size.getZ()