跳到主要内容

SPH 流体求解器

SPH 用一组粒子表示液体。LambdaSim 的 SolverSPH 使用 DFSPH 方法,在计算水流运动时尽量保持液体体积稳定,适合烧杯盛水、液体绕过障碍物等场景。

本节以仓库中的两个示例介绍使用流程:准备容器和初始水量 → 设置求解器 → 播放并查看液面。

运行两个示例​

示例场景与用途
烧杯静水在 250 mL 烧杯中加入 150 mL 水,了解基本配置。
烧杯与静置搅拌棒在相同水量下加入一根倾斜的固定玻璃棒,了解如何添加障碍物。

在已配置好 LambdaSim、Ektro、AlphaCore、LambdaEngine GUI 和渲染依赖的环境中,从 LambdaSim 仓库根目录分别运行:

PYTHONPATH=.:src:lambda_engine/src .venv/bin/python \
examples/fluids/dfsph_standing_beaker_water.py --device cuda:0

PYTHONPATH=.:src:lambda_engine/src .venv/bin/python \
examples/fluids/dfsph_beaker_water_with_stirring_rod.py --device cuda:0

这里显式选择 cuda:0 进行物理计算。脚本的 --device 默认值是 cpu;渲染设备由 --render-device 和 --render-device-id 单独选择,默认也是 CUDA 的第 0 张卡。

启动后点击时间轴的 Play 计算,回到 第 1 帧恢复初始状态。两个示例均以 60 fps 设置时间轴,演示 120 帧。搅拌棒始终固定在预设位置,液体会受它阻挡;此例没有棒的搅拌运动、浮动或流体推动刚体的效果。

烧杯与静置搅拌棒示例在 LambdaEngine 中的运行效果

如图(上): “烧杯与静置搅拌棒”示例在 LambdaEngine 中的运行效果。视口显示烧杯、重建液面和倾斜的玻璃棒;右侧可调整 Lambda Render 渲染参数,下方时间轴控制仿真播放。

默认使用 Lambda Render 显示透明液面和环境光。添加 --render-mode opengl 可使用 OpenGL 预览。设备支持条件 Graph 时,可添加 --use_graph,让 GPU 控制 DFSPH 的迭代停止;不支持时会报错,CPU 请使用默认模式。

数据准备​

容器与障碍物​

两个示例所需资产已随仓库提供:

路径用途
tests/assets/fluid/beaker_250ml.usda烧杯外观、挡水模型和可注水区域。
tests/assets/fluid/stirring_rod.usda玻璃棒外观及挡水模型。
tests/assets/materials/glass_material.usda重建液面的显示材质。
examples/fluids/assets/lebombo_lights.usda环境灯光,引用同目录下自带的 HDR 贴图。

流体资产的四种角色​

同一烧杯的外观、刚体碰撞、流体碰撞与内腔四种角色

如图(上): 同一个容器可以用四种角色描述,分别负责外观、刚体接触、挡水和初始注水范围。它们按用途配置,无需每个物体都具备全部四种角色。

替换自己的容器时,需要在 USD 中用 ektro:role 标记各类 Mesh 的用途。节点名称本身不能代替这个属性。

ektro:role作用USD 的 purpose
visual显示物体外观,可有多个。default
rigid_collision供刚体求解器处理物体接触,可拆成多个碰撞块;只做流体阻挡时可省略。guide
fluid_collision表示杯壁、杯底或障碍物的实体部分,用来挡住水;每个边界必须有且只有一个。guide
vessel_interior表示容器内部可注水的空间,用来生成初始粒子;用于注水的容器需要一个,普通障碍物可省略。guide

计算用的 fluid_collision 和 vessel_interior 都需要闭合的三角网格,并设置 orientation="rightHanded"。杯壁模型应有厚度,内外表面在杯口边缘连接,杯口仍可让水通过;内腔模型则需要在口部封闭,以界定可注水空间。这两个计算模型保持启用,purpose="guide" 会让它们不参与外观渲染。

资产以米为单位,设置 metersPerUnit=1。上述两个计算模型不挂 PhysicsCollisionAPI;如果资产另有供刚体使用的 rigid_collision,保留其 purpose="guide" 和 PhysicsCollisionAPI。

Vessel 与不含内腔的流体碰撞物​

这里的 Vessel 指可以盛放液体的容器,例如烧杯或碗。本例用名为 Vessel 的根节点组织容器的各类几何;这个节点名可以变化,实际用途由其下面的角色决定。vessel_interior 描述可注水空间,初始水量由后面的 volume 单独指定。

Vessel 烧杯与没有内腔的玻璃棒对比,以及边界路径和注水路径的选择

如图(上): 左侧烧杯提供内腔,可以作为初始注水区域;右侧玻璃棒只有 visual 和 fluid_collision,只阻挡液体,不提供注水区域。初始水粒子会避开棒占据的空间。图中绿色内腔顶部的虚拟封口只界定注水范围,不参与挡水,真实杯口保持开放。

两个对象的根节点都要加入 boundaries 和 solver.boundary_paths;VolumeSampling.region_path 指向烧杯内腔:

用途本例场景路径
烧杯边界根/root/obj/Beaker/Vessel
玻璃棒边界根/root/obj/Rod/World/Rod
初始注水区域/root/obj/Beaker/Vessel/vessel_interior

路径包含导入对象名和资产内部层级;更换对象名或 USD 层级时,要同步修改这些路径。

初始水量与物性​

下面提取静水示例的核心配置,可从仓库根目录执行。先加载烧杯,再声明水量;粒子会在初始化时自动生成。

from lambdasim.scene.scene import Scene
from lambdasim.fluid_entity import FluidEntity, VolumeSampling, build_fluid_entity
from lambdasim.solvers.solver_sph import SolverSPH, DFSPHOptions, SPHSurfaceTension

scene = Scene()
scene.add_usd__ektro("tests/assets/fluid/beaker_250ml.usda", name="Beaker")
BEAKER_ROOT = "/root/obj/Beaker/Vessel"

water = FluidEntity(
path="/root/obj/Water",
rest_density=1000.0, # 密度,kg/m³
kinematic_viscosity=1.0e-6, # 运动黏度,m²/s
initial_state=VolumeSampling(
region_path=BEAKER_ROOT + "/vessel_interior",
volume=150e-6, # m³,即 150 mL
velocity=(0.0, 0.0, 0.0), # 初速度,m/s
wall_clearance=0.002, # 初始粒子距固体表面的最小距离,m
volume_tolerance=4e-9, # 初始水量允许的取整误差,m³
),
)
scene.set_fluid_dynamics_scene(build_fluid_entity(
scene,
entities=(water,),
boundaries=(BEAKER_ROOT,),
))

water.path 是这份液体的标识,不需要事先在 USD 中创建 Water 模型。region_path 则必须指向已导入容器的内腔。

初始化沿重力反方向从低处向高处填充,并避开所有选中的固体边界。两个示例采用 Z 轴向上、重力朝 −Z 的设置;自定义重力可在 build_fluid_entity() 中传入 gravity=(0.0, 0.0, -9.81),单位为 m/s²。

volume 使用立方米,1 mL = 1e-6 m³。每个粒子代表的初始体积为 particle_spacing³,因此总水量需要按粒子数量取整;volume_tolerance 控制这一步允许的误差。wall_clearance 至少为粒子间距的一半,本例取一个粒子间距,即 2 mm。

设置 DFSPH 求解器​

接着上一段代码创建求解器,选择液体和边界,再初始化:

solver = SolverSPH(
particle_spacing=0.002,
world_bounds=((-0.08, -0.08, -0.03), (0.25, 0.08, 0.42)),
substep_count=8,
surface_tension=SPHSurfaceTension(
model="becker2007", fluid_coefficient=0.0, boundary_coefficient=0.0,
),
dfsph_options=DFSPHOptions(
density_error_percent=0.5,
divergence_error_percent=1.0,
min_density_iterations=2,
max_density_iterations=300,
min_divergence_iterations=1,
max_divergence_iterations=100,
relaxation_factor=0.5,
adaptive_execution="host",
),
)
solver.src_paths = [water.path]
solver.boundary_paths = [BEAKER_ROOT]
scene.add_solver__ektro(solver)
scene.finalize__ektro(device="cuda:0", num_envs=1)

这两段代码完成物理场景初始化;打开编辑器、设置灯光和显示液面的完整流程见前面的示例脚本。

常用参数​

下表列出两个示例实际采用的值。

参数示例值含义与调整方式
particle_spacing0.002 m粒子间距。越小越能表现细节,粒子数量和计算量也越大。
world_bounds见代码计算范围的最小、最大坐标,应覆盖液体可能到达的位置。
substep_count静水 8,有棒 16将每帧拆成几小步计算。增加通常有助于稳定,但会增加耗时。
rest_density1000.0 kg/m³水的目标密度,在 FluidEntity 中设置。
kinematic_viscosity1e-6 m²/s液体运动的黏滞程度,在 FluidEntity 中设置;增大后流动更黏滞。
density_error_percent0.5控制密度偏离目标值的平均误差,数值越小,要求越严格。
divergence_error_percent1.0控制流动导致局部体积变化的平均误差,帮助液体保持体积。
min/max_density_iterations2 / 300每个子步中,修正密度的最少、最多轮数。
min/max_divergence_iterations1 / 100每个子步中,修正体积变化趋势的最少、最多轮数。
relaxation_factor0.5每轮修正的力度,初次使用可保留示例值。
adaptive_execution"host"由 CPU 判断何时停止迭代;"graph" 在支持的设备上由 GPU 判断。物理计算设备仍由 device 决定。

误差参数按百分数填写:0.5 表示 0.5%。示例沿用默认的百分比判断和自动停止迭代;直接使用 DFSPHOptions() 时,两项误差默认是更严格的 0.01 和 0.1,与本例不同。

表面张力与贴壁效果​

表面张力让液面有收拢、变圆的趋势。通过 SPHSurfaceTension 选择模型,并分别设置液体自身和液体与固体之间的作用强度。

设置作用
model="becker2007"以粒子间的吸引作用表现表面张力,是默认模型。
model="akinci2013"同时考虑粒子间作用和液面的弯曲情况,表现表面的收拢效果。
fluid_coefficient液体自身的表面张力强度;0 表示关闭这一作用。
boundary_coefficient液体对固体表面的吸附强度,影响贴壁效果;0 表示关闭这一作用。

两个示例的两个系数都是 0.0,因此没有启用表面张力或壁面吸附。 使用时可在创建求解器的位置改选模型,将相应系数设为正值,并逐项观察液面变化。

这两个系数是模型直接使用的非负参数,不能直接当作以 N/m 为单位的物理表面张力填写。切换模型或粒子间距后,应重新检查效果。黏度控制流动的黏滞程度,表面张力控制液面的收拢,两者需要分别设置。

在水中加入搅拌棒​

第二个示例沿用相同的水量、密度和黏度。在调用 build_fluid_entity() 之前加载并摆放玻璃棒:

import ektro as ek

rod = scene.add_usd__ektro("tests/assets/fluid/stirring_rod.usda", name="Rod")
rod.set_RT(ek.vec3(-0.014, 0.0, 0.0055), ek.vec3(0.0, 25.0, 0.0))
ROD_ROOT = "/root/obj/Rod/World/Rod"

随后将 build_fluid_entity() 的 boundaries 改为 (BEAKER_ROOT, ROD_ROOT),将 solver.boundary_paths 改为 [BEAKER_ROOT, ROD_ROOT],并在创建 SolverSPH 时设置 substep_count=16。水的 region_path 仍指向烧杯内腔。

棒的直径为 6 mm、长度为 180 mm,上述设置使其绕 Y 轴倾斜 25°。棒只有外观和挡水模型,没有注水区域;初始化会避开棒占据的空间。摆放时要让棒与杯壁、杯底的挡水模型留有间隙,模型相交或零间隙接触会被拒绝。

查看液面与修改配置​

两个脚本的 build_surface() 使用 FluidSurfaceVisualization 将粒子转换成可显示的液面。voxel_size=0.001 表示液面重建网格间距为 1 mm;减小它可增加显示细节,也会增加重建和渲染开销。bounds_lower、bounds_upper 限定液面显示范围,更换容器或增加水量时也需要检查。

液面重建参数和材质只影响显示。改变水量、流动或表面张力,应修改前文的液体与求解器配置。两个示例默认使用 spp=128、max_bounces=16;需要更快预览时,可在编辑器渲染参数面板降低采样数。

初始化后,可查看实际生效的物性和求解器设置:

config = solver.get_effective_config()
print(config["physics"])
print(config["dfsph"])

修改这个查询结果不会改变仿真。调整水量、粒子间距、子步数或表面张力等设置后,重新运行脚本;回到第 1 帧只恢复本次初始化的状态。