EG/plugins/user/particle_system/tools/particle_utils.py
2025-12-12 16:16:15 +08:00

459 lines
13 KiB
Python
Raw Permalink 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.

"""
粒子工具函数
提供粒子系统相关的实用工具函数
"""
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