459 lines
13 KiB
Python
459 lines
13 KiB
Python
"""
|
||
粒子工具函数
|
||
提供粒子系统相关的实用工具函数
|
||
"""
|
||
|
||
import math
|
||
import random
|
||
from typing import List, Tuple, Optional
|
||
from panda3d.core import Vec3, Point3, LVector3f, Quat
|
||
|
||
class ParticleUtils:
|
||
"""
|
||
粒子系统工具类
|
||
提供各种实用的粒子计算和生成函数
|
||
"""
|
||
|
||
@staticmethod
|
||
def lerp(a: float, b: float, t: float) -> float:
|
||
"""
|
||
线性插值
|
||
|
||
Args:
|
||
a: 起始值
|
||
b: 结束值
|
||
t: 插值参数 (0-1)
|
||
|
||
Returns:
|
||
插值结果
|
||
"""
|
||
return a + (b - a) * t
|
||
|
||
@staticmethod
|
||
def lerp_vec3(a: Vec3, b: Vec3, t: float) -> Vec3:
|
||
"""
|
||
Vec3线性插值
|
||
|
||
Args:
|
||
a: 起始向量
|
||
b: 结束向量
|
||
t: 插值参数 (0-1)
|
||
|
||
Returns:
|
||
插值结果向量
|
||
"""
|
||
return Vec3(
|
||
ParticleUtils.lerp(a.x, b.x, t),
|
||
ParticleUtils.lerp(a.y, b.y, t),
|
||
ParticleUtils.lerp(a.z, b.z, t)
|
||
)
|
||
|
||
@staticmethod
|
||
def smooth_step(t: float) -> float:
|
||
"""
|
||
平滑步进函数
|
||
|
||
Args:
|
||
t: 输入值 (0-1)
|
||
|
||
Returns:
|
||
平滑后的值
|
||
"""
|
||
t = max(0.0, min(1.0, t))
|
||
return t * t * (3.0 - 2.0 * t)
|
||
|
||
@staticmethod
|
||
def ease_in_out(t: float) -> float:
|
||
"""
|
||
缓入缓出函数
|
||
|
||
Args:
|
||
t: 输入值 (0-1)
|
||
|
||
Returns:
|
||
缓动后的值
|
||
"""
|
||
if t < 0.5:
|
||
return 2.0 * t * t
|
||
else:
|
||
return -1.0 + (4.0 - 2.0 * t) * t
|
||
|
||
@staticmethod
|
||
def random_in_sphere(radius: float = 1.0) -> Vec3:
|
||
"""
|
||
在球体内生成随机点
|
||
|
||
Args:
|
||
radius: 球体半径
|
||
|
||
Returns:
|
||
随机点坐标
|
||
"""
|
||
# 使用拒绝采样法
|
||
while True:
|
||
x = random.uniform(-1, 1)
|
||
y = random.uniform(-1, 1)
|
||
z = random.uniform(-1, 1)
|
||
|
||
if x*x + y*y + z*z <= 1.0:
|
||
return Vec3(x, y, z) * radius
|
||
|
||
@staticmethod
|
||
def random_on_sphere(radius: float = 1.0) -> Vec3:
|
||
"""
|
||
在球面上生成随机点
|
||
|
||
Args:
|
||
radius: 球体半径
|
||
|
||
Returns:
|
||
随机点坐标
|
||
"""
|
||
# 使用正态分布方法
|
||
x = random.gauss(0, 1)
|
||
y = random.gauss(0, 1)
|
||
z = random.gauss(0, 1)
|
||
|
||
length = math.sqrt(x*x + y*y + z*z)
|
||
if length > 0:
|
||
return Vec3(x/length, y/length, z/length) * radius
|
||
else:
|
||
return Vec3(0, 0, radius)
|
||
|
||
@staticmethod
|
||
def random_in_circle(radius: float = 1.0) -> Vec3:
|
||
"""
|
||
在圆形内生成随机点 (Z=0)
|
||
|
||
Args:
|
||
radius: 圆形半径
|
||
|
||
Returns:
|
||
随机点坐标
|
||
"""
|
||
angle = random.uniform(0, 2 * math.pi)
|
||
r = random.uniform(0, radius)
|
||
return Vec3(r * math.cos(angle), r * math.sin(angle), 0)
|
||
|
||
@staticmethod
|
||
def random_on_circle(radius: float = 1.0) -> Vec3:
|
||
"""
|
||
在圆周上生成随机点 (Z=0)
|
||
|
||
Args:
|
||
radius: 圆形半径
|
||
|
||
Returns:
|
||
随机点坐标
|
||
"""
|
||
angle = random.uniform(0, 2 * math.pi)
|
||
return Vec3(radius * math.cos(angle), radius * math.sin(angle), 0)
|
||
|
||
@staticmethod
|
||
def random_in_box(dimensions: Vec3) -> Vec3:
|
||
"""
|
||
在盒子内生成随机点
|
||
|
||
Args:
|
||
dimensions: 盒子尺寸
|
||
|
||
Returns:
|
||
随机点坐标
|
||
"""
|
||
return Vec3(
|
||
random.uniform(-dimensions.x/2, dimensions.x/2),
|
||
random.uniform(-dimensions.y/2, dimensions.y/2),
|
||
random.uniform(-dimensions.z/2, dimensions.z/2)
|
||
)
|
||
|
||
@staticmethod
|
||
def random_cone_direction(direction: Vec3, cone_angle: float) -> Vec3:
|
||
"""
|
||
在锥形范围内生成随机方向
|
||
|
||
Args:
|
||
direction: 锥形中心方向
|
||
cone_angle: 锥角 (弧度)
|
||
|
||
Returns:
|
||
随机方向向量
|
||
"""
|
||
# 生成锥形内的随机方向
|
||
phi = random.uniform(0, 2 * math.pi)
|
||
cos_theta = random.uniform(math.cos(cone_angle), 1.0)
|
||
sin_theta = math.sqrt(1.0 - cos_theta * cos_theta)
|
||
|
||
# 局部坐标系中的方向
|
||
local_dir = Vec3(
|
||
sin_theta * math.cos(phi),
|
||
sin_theta * math.sin(phi),
|
||
cos_theta
|
||
)
|
||
|
||
# 转换到世界坐标系
|
||
# 简化实现:假设direction是(0,0,1)
|
||
if abs(direction.z - 1.0) < 0.001:
|
||
return local_dir
|
||
elif abs(direction.z + 1.0) < 0.001:
|
||
return Vec3(local_dir.x, -local_dir.y, -local_dir.z)
|
||
else:
|
||
# 需要完整的旋转变换
|
||
# 这里使用简化版本
|
||
return local_dir
|
||
|
||
@staticmethod
|
||
def calculate_billboard_rotation(position: Point3, camera_pos: Point3) -> Quat:
|
||
"""
|
||
计算广告牌旋转
|
||
|
||
Args:
|
||
position: 广告牌位置
|
||
camera_pos: 摄像机位置
|
||
|
||
Returns:
|
||
旋转四元数
|
||
"""
|
||
to_camera = camera_pos - position
|
||
to_camera.normalize()
|
||
|
||
# 计算旋转四元数
|
||
up = Vec3(0, 0, 1)
|
||
right = to_camera.cross(up)
|
||
|
||
if right.lengthSquared() < 0.001:
|
||
# 摄像机在正上方或正下方
|
||
right = Vec3(1, 0, 0)
|
||
|
||
right.normalize()
|
||
up = right.cross(to_camera)
|
||
up.normalize()
|
||
|
||
# 构建旋转矩阵并转换为四元数
|
||
# 简化实现
|
||
return Quat()
|
||
|
||
@staticmethod
|
||
def apply_noise(value: float, noise_strength: float, time: float = 0.0) -> float:
|
||
"""
|
||
应用噪声到数值
|
||
|
||
Args:
|
||
value: 原始值
|
||
noise_strength: 噪声强度
|
||
time: 时间参数
|
||
|
||
Returns:
|
||
添加噪声后的值
|
||
"""
|
||
# 简单的伪随机噪声
|
||
noise = math.sin(value * 12.9898 + time * 78.233) * 43758.5453
|
||
noise = noise - math.floor(noise) # 取小数部分
|
||
noise = (noise - 0.5) * 2.0 # 转换到 -1 到 1
|
||
|
||
return value + noise * noise_strength
|
||
|
||
@staticmethod
|
||
def calculate_drag_force(velocity: Vec3, drag_coefficient: float,
|
||
air_density: float = 1.225) -> Vec3:
|
||
"""
|
||
计算阻力
|
||
|
||
Args:
|
||
velocity: 速度向量
|
||
drag_coefficient: 阻力系数
|
||
air_density: 空气密度
|
||
|
||
Returns:
|
||
阻力向量
|
||
"""
|
||
speed_squared = velocity.lengthSquared()
|
||
if speed_squared < 0.001:
|
||
return Vec3(0, 0, 0)
|
||
|
||
speed = math.sqrt(speed_squared)
|
||
drag_magnitude = 0.5 * air_density * drag_coefficient * speed_squared
|
||
|
||
# 阻力方向与速度相反
|
||
drag_direction = -velocity / speed
|
||
|
||
return drag_direction * drag_magnitude
|
||
|
||
@staticmethod
|
||
def calculate_buoyancy_force(volume: float, fluid_density: float = 1000.0,
|
||
gravity: float = 9.81) -> float:
|
||
"""
|
||
计算浮力
|
||
|
||
Args:
|
||
volume: 物体体积
|
||
fluid_density: 流体密度
|
||
gravity: 重力加速度
|
||
|
||
Returns:
|
||
浮力大小
|
||
"""
|
||
return volume * fluid_density * gravity
|
||
|
||
@staticmethod
|
||
def distance_squared(a: Point3, b: Point3) -> float:
|
||
"""
|
||
计算两点间距离的平方
|
||
|
||
Args:
|
||
a: 点A
|
||
b: 点B
|
||
|
||
Returns:
|
||
距离的平方
|
||
"""
|
||
dx = a.x - b.x
|
||
dy = a.y - b.y
|
||
dz = a.z - b.z
|
||
return dx*dx + dy*dy + dz*dz
|
||
|
||
@staticmethod
|
||
def clamp(value: float, min_val: float, max_val: float) -> float:
|
||
"""
|
||
限制数值范围
|
||
|
||
Args:
|
||
value: 输入值
|
||
min_val: 最小值
|
||
max_val: 最大值
|
||
|
||
Returns:
|
||
限制后的值
|
||
"""
|
||
return max(min_val, min(max_val, value))
|
||
|
||
@staticmethod
|
||
def wrap_angle(angle: float) -> float:
|
||
"""
|
||
将角度限制在 -π 到 π 范围内
|
||
|
||
Args:
|
||
angle: 输入角度 (弧度)
|
||
|
||
Returns:
|
||
限制后的角度
|
||
"""
|
||
while angle > math.pi:
|
||
angle -= 2 * math.pi
|
||
while angle < -math.pi:
|
||
angle += 2 * math.pi
|
||
return angle
|
||
|
||
@staticmethod
|
||
def create_color_gradient(colors: List[Vec3], positions: List[float], t: float) -> Vec3:
|
||
"""
|
||
创建颜色渐变
|
||
|
||
Args:
|
||
colors: 颜色列表
|
||
positions: 位置列表 (0-1)
|
||
t: 插值参数 (0-1)
|
||
|
||
Returns:
|
||
插值后的颜色
|
||
"""
|
||
if not colors or not positions or len(colors) != len(positions):
|
||
return Vec3(1, 1, 1)
|
||
|
||
if len(colors) == 1:
|
||
return colors[0]
|
||
|
||
# 找到插值区间
|
||
for i in range(len(positions) - 1):
|
||
if t <= positions[i + 1]:
|
||
# 在区间 [i, i+1] 内插值
|
||
local_t = (t - positions[i]) / (positions[i + 1] - positions[i])
|
||
return ParticleUtils.lerp_vec3(colors[i], colors[i + 1], local_t)
|
||
|
||
# 超出范围,返回最后一个颜色
|
||
return colors[-1]
|
||
|
||
@staticmethod
|
||
def create_size_curve(sizes: List[float], positions: List[float], t: float) -> float:
|
||
"""
|
||
创建尺寸曲线
|
||
|
||
Args:
|
||
sizes: 尺寸列表
|
||
positions: 位置列表 (0-1)
|
||
t: 插值参数 (0-1)
|
||
|
||
Returns:
|
||
插值后的尺寸
|
||
"""
|
||
if not sizes or not positions or len(sizes) != len(positions):
|
||
return 1.0
|
||
|
||
if len(sizes) == 1:
|
||
return sizes[0]
|
||
|
||
# 找到插值区间
|
||
for i in range(len(positions) - 1):
|
||
if t <= positions[i + 1]:
|
||
# 在区间 [i, i+1] 内插值
|
||
local_t = (t - positions[i]) / (positions[i + 1] - positions[i])
|
||
return ParticleUtils.lerp(sizes[i], sizes[i + 1], local_t)
|
||
|
||
# 超出范围,返回最后一个尺寸
|
||
return sizes[-1]
|
||
|
||
@staticmethod
|
||
def generate_spiral_positions(count: int, radius: float, height: float,
|
||
turns: float = 2.0) -> List[Point3]:
|
||
"""
|
||
生成螺旋形位置
|
||
|
||
Args:
|
||
count: 点的数量
|
||
radius: 螺旋半径
|
||
height: 螺旋高度
|
||
turns: 螺旋圈数
|
||
|
||
Returns:
|
||
位置列表
|
||
"""
|
||
positions = []
|
||
|
||
for i in range(count):
|
||
t = i / max(1, count - 1)
|
||
angle = t * turns * 2 * math.pi
|
||
z = t * height
|
||
|
||
x = radius * math.cos(angle)
|
||
y = radius * math.sin(angle)
|
||
|
||
positions.append(Point3(x, y, z))
|
||
|
||
return positions
|
||
|
||
@staticmethod
|
||
def calculate_particle_bounds(positions: List[Point3]) -> Tuple[Point3, Point3]:
|
||
"""
|
||
计算粒子边界框
|
||
|
||
Args:
|
||
positions: 粒子位置列表
|
||
|
||
Returns:
|
||
(最小点, 最大点)
|
||
"""
|
||
if not positions:
|
||
return Point3(0, 0, 0), Point3(0, 0, 0)
|
||
|
||
min_pos = Point3(positions[0])
|
||
max_pos = Point3(positions[0])
|
||
|
||
for pos in positions[1:]:
|
||
min_pos.x = min(min_pos.x, pos.x)
|
||
min_pos.y = min(min_pos.y, pos.y)
|
||
min_pos.z = min(min_pos.z, pos.z)
|
||
|
||
max_pos.x = max(max_pos.x, pos.x)
|
||
max_pos.y = max(max_pos.y, pos.y)
|
||
max_pos.z = max(max_pos.z, pos.z)
|
||
|
||
return min_pos, max_pos |