ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity流体模拟实战:Obi Fluid插件SPH求解器原理与调参指南

Unity流体模拟实战:Obi Fluid插件SPH求解器原理与调参指南 简介面向Unity3D开发者的Obi Fluid流体插件入门教程与可运行源码包以粒子系统为核心帮助刚接触流体模拟的新手快速掌握水流、烟雾、火焰等特效的制作思路。教程内容围绕粒子技术的交互性设计、可视化编辑器使用和脚本API控制策略展开详细说明粒子产生、运动、碰撞等物理行为以及重力、粘度、表面张力等属性对模拟效果的影响同时兼顾项目规划、开发调试和性能优化等实践环节为从入门到进阶提供清晰路线。压缩包内共六个文件以JavaScript、HTML、JSON和Markdown为主要类型JS脚本承载流体模拟与交互逻辑HTML搭建可操作的演示界面JSON存放参数配置Markdown文档提供步骤讲解另附初始化与版本管理文件整体仅11KB结构轻巧可直接运行并按需修改。目前已有88人学习浏览。通过这份源码开发者能观察粒子生成、碰撞到渲染的完整流程还能借助文档调整粒子形状、颜色、材质与光照等视觉细节并结合Unity的摄像机、光源等组件增强逼真度也能通过脚本API扩展自定义流体行为既能作为教学素材也可充当项目开发的原型基础。1. Obi Fluid插件不是粒子系统平替是能出真实流体的物理求解器做游戏特效和交互装置的人多半和我一样被流体折磨过。Unity自带的Particle System能喷烟吐火但你想做一杯倒出来的水、一坨黏糊糊的胶体它立刻露馅——没有碰撞、没有聚合、没有表面张力。Obi Fluid插件解决的就是这个它用粒子加SPH求解器模拟连续流体能跑出真实的水、油、黏液。这个教程资源带可运行源码适合那些不想从零写求解器、又希望拿到手就能改参数出效果的人。我拆完后最大的感受是它上手不难但调参和避坑的细节全在源码和场景配置里。2. 跑通可运行源码Unity环境检查与Demo导入全流程2.1 先看包结构别一解压就双击场景拿到这个“Obi Fluid插件教程[可运行源码]”包后我建议先不要急着打开场景因为Obi Fluid是原生插件加C#脚本的混合体缺依赖或者版本不对场景会全部变成粉红色或报一堆Script Missing。常见做法是先把压缩包解压到非中文路径用Unity Hub新建一个URP或Built-in项目再把包里的Assets文件夹内容整体拷进去。注意如果你的项目使用了URPObi Fluid的某些Shader可能需要手动升级否则液面会渲染成半透明白板。整个包的核心结构大概是这样的我拆完后按重要程度排了个序Obi/Scripts核心求解器、发射器、碰撞体的C#源码所有可运行逻辑都在这里。里面又分Solver、Collider、Emitter几个子目录互有依赖别乱删。Obi/Resources蓝图Blueprint资产和默认材质。蓝图是粒子初始排列、密度、半径等参数的集合相当于物理配置的模板后面调参数主要就是调它。Obi/Rendering粒子渲染用的Shader与材质。水感、泡沫感靠它们如果你发现液体像一堆彩色豆子多半是Shader没有正确编译。Demo一个或几个完整的演示场景里面预设了多种流体蓝图的对比效果。我建议你从Demo场景里最小最简陋的那个开始跑别一上来就打开“海洋泡沫”那种重型场景。这里有一个很多人会忽略的点源码包里的C#脚本文件名不能改连命名空间都不能动。Obi Fluid的运行时用了很多[ObiMonoBehaviour]特性绑定你一旦重命名类或命名空间所有预制体和场景里的引用都会断掉。我吃过这个亏当时觉得某个文件名太长改成缩写结果整个项目全红。2.2 Unity版本与依赖项检查Obi Fluid适合2020 LTS以上版本原因不是官方偷懒而是底层依赖了Unity的Job System和Burst Compiler。旧版Unity的这两个模块不完整求解器跑起来性能会差很多甚至直接报错。检查方法很简单打开Packages/manifest.json看依赖列表里有没有com.unity.burst、com.unity.collections、com.unity.jobs这三项。如果漏了Unity在加载Obi插件时会报“InvalidOperationException: The Burst compiler was not properly initialized”。这个异常不是Obi的问题纯粹是环境缺依赖。处理方法是编辑manifest.json加上下面几行然后让Unity重新编译{ com.unity.burst: 1.8.12, com.unity.collections: 1.2.4, com.unity.jobs: 0.70.0 }这里要说明版本号是我实际项目里用的Unity的Package Manager会根据你项目的Unity版本自动解析兼容版本。如果填的版本和你当前引擎不匹配一般会警告但不会拉跨整个项目。更稳的做法是打开Package Manager窗口在UPM里搜索这几个包点Install让Unity自己决定版本。改完manifest后让Unity重新加载项目看Console窗口有没有红色报错。没有错误就可以进入下一步。顺便提一句如果你的项目同时装了其他物理插件比如Particle System的第三方扩展可能会和Obi Fluid抢Burst的编译资源。我建议在跑Obi Demo时暂时把其他插件禁用减少变量。2.3 导入后跑通自带Demo的标准流程接下来这一步直接决定你后面的体验。第一步用Unity打开项目在Project窗口找到Demo场景双击打开。先别按Play我们要做三件事。第一把Game视图的Resolution调成16:9。这个插件启动时会重新分配粒子缓冲区如果分辨率极端可能触发宽高比相关的shader问题。第二检查Project Settings里Physics的Gravity是否为(0, -9.8, 0)。Obi Fluid用的是自己的求解器不走Unity的PhysX但场景里如果有刚体碰撞体仍会受PhysX影响保持一致会更顺。第三打开Window Obi Obi Solver面板看一下Solver组件上的MaxParticles数值。默认可能是100000或更高如果你的电脑不是游戏本建议先调到50000保证第一遍能跑流畅。这三项都设置好之后点击Play。不出意外你能看到液体从发射器里喷出来落在容器里聚成一滩有表面张力的水。注意观察画面右上角或Console窗口里的FPS如果低于30说明你的粒子数对机器来说太高了。常见做法是把刚才的MaxParticles再往下调或者打开ObiSolver组件的Threads配置调整并行线程数。我个人经验是线程数设为你CPU物理核心的一半最稳超线程反而会导致缓存抖动。# 如果源码包是用git管理的也可以用命令行跑一遍确认文件完整 cd ObiFluidTutorial git status --porcelain | head -20上面这段命令是示意实际你解压后可能没有.git目录。我写出来是为了说明一个习惯跑Demo前先检查是否有缺失文件。如果你解压后莫名其妙的报错多半是压缩包解压到一半被杀毒软件误删了。Obi Fluid的C#脚本有时会被某些杀毒软件当作可疑文件建议加入白名单再重新解压。这一步的逻辑是提前排除文件完整性导致的问题避免后续在本不是问题的地方浪费几个小时。跑通Demo后很多人会立刻去改参数。先别急下一步我们先把参数和物理求解器的关系理清楚否则你会被一堆英文Slider淹没。3. 核心参数与物理求解器把水、油、黏液调出来的旋钮位置3.1 粒子半径与分辨率先决定你能跑多大场景Obi Fluid底层是SPH平滑粒子流体动力学所以第一个要理解的参数就是粒子半径。这个值不是设大设小这么简单它会直接改变粒子的影响范围也决定了同样体积下需要多少粒子。半径越小需要填充的粒子就越多液面细节越丰富但求解器的计算量成倍上涨。我给一个经验映射表这是我在几个交互项目里试出来的不是官方手册标准但很适合落地预期效果粒子半径米推荐粒子数上限适用场景粗颗粒特效0.05 - 0.082万 - 5万卡通水花、魔法液体不追求真实感较真实的水0.02 - 0.045万 - 20万产品演示、小范围倒水、游戏技能极高细节流体质感0.01 - 0.01520万以上电影级预计算慎用实时交互注意粒子半径只是其中一个维度真正让流体看起来“像水”的是粘度、表面张力和浮力这几个参数的组合。我一般会先固定一个中等半径比如0.03只调其他参数等场景形态定了之后再尝试增加分辨率。如果你一开始就追细节最后往往只能得到一个跑不动的Demo。补充一点Obi Fluid的粒子半径是定义在蓝图里的跟场景物体的Scale无关。你缩放容器粒子不会跟着变大只会改变碰撞体的边界。很多新手以为把容器放大粒子就变粗这是误解。3.2 迭代次数与约束强度性能与稳定性的天平Obi Fluid的求解器每个物理帧会多次迭代约束求解。迭代次数的数量级很小通常从1到10。这个值越高流体越“硬”不容易被压缩也不会像果冻一样软绵绵。但每帧耗时也跟着涨。移动端我一般用2到3次迭代PC上4到5次就够超过6次收益非常低纯浪费计算资源。再配合两个约束系数Pressure C控制压力约束强度Tensile C控制张力约束强度。这里有个血泪经验如果你看到粒子像爆炸一样乱飞先别怀疑参数先把压力约束调低或者把迭代次数加一档。很多时候不是参数不对而是粒子碰撞Particle Collision没开粒子互相穿透导致求解器算出了极端的压力值。你可以在ObiSolver组件上勾选Particle Collision但注意它会增加一次近邻搜索耗时大概多15%。在移动端如果场景本身粒子数少可以开粒子数多的话建议保持关闭。还有一个容易被忽略的开关叫Sleep Threshold。这个值设高了粒子会过早“睡觉”表现为流体静止时像一坨定型泥浆碰一下才碎。我一般把它设成0.005左右太小会导致粒子永远醒着CPU消耗高。3.3 黏度、表面张力与流体类型的映射下面这组参数是我在多个项目里试出来比较稳的起点值直接照着给蓝图赋上就能得到对应的液体观感。水的典型参数Viscosity粘度0.05Surface Tension表面张力0.3Buoyancy浮力0.8Restitution反弹0.1油或黏稠液体Viscosity0.8 - 1.2Surface Tension0.5Buoyancy0.2Restitution0.05熔岩或高密度流体Viscosity1.5Surface Tension0.7Buoyancy0还需要在ObiEmitter上启用温度相关选项否则看起来就是一层红油。这些参数在蓝图资产上调整不是场景里的ObiEmitter。我见过有人改了Emitter的Speed结果液体形态完全没变反而觉得插件有bug。实际上蓝图的物理属性才是核心。所有引用同一蓝图的发射器会共享这一套物理配置所以你要做多种液体就多建几个蓝图资产。操作上在Project窗口右键 Create Obi Fluid Blueprint然后可以复制现有的Demo蓝图改成不同参数的变体。改完参数后Unity需要重新进入Play模式才生效编辑器模式下修改蓝图不会实时刷新到场景。如果你想在同一场景里对比水和油的效果最简单的方法是创建两个ObiEmitter分别引用两份蓝图而不是试图在运行时动态换蓝图。4. 自定义流体交互从发射器到鼠标拖拽的完整脚本4.1 搭建一个可发射流体的小场景跑通Demo后就该自己做场景了。新建一个场景创建一个空物体挂上ObiEmitter组件注意ObiEmitter本身不负责发射位置它需要一个发射形状。Obi Fluid里常见做法是给ObiEmitter挂一个ObiEmitterShape子物体Shape可以是圆盘、球体或者方形。这个Shape的Scale决定了发射口的半径我会先设成0.2米左右这样流体出来是一束而不是一条线。容器方面直接用Unity自带的Collider做不了碰撞。你需要创建一个平面或圆柱体挂上ObiCollider组件。注意Box Collider和Mesh Collider都支持但ObiCollider组件必须显式挂在物体上不能挂在子物体再靠父子继承。这是我的一个翻车点我把ObiCollider挂在父物体上子物体的Mesh带Collider结果粒子直接穿过子物体地板。如果你要做一个杯子或者水槽我建议用几个Plane围成U形每个Plane单独挂ObiCollider。比用一个Mesh Collider更快而且不用担心非凸Mesh的碰撞性能问题。ObiCollider上有一个Collision Material槽位这里最好填上同一个ObiCollisionMaterial资产。这个资产控制摩擦和反弹默认资产在Obi的Resources文件夹里也可以自己创建。4.2 脚本控制发射、停止与颜色变换发射器的核心参数是ObiEmitter.speed和Capacity。speed控制粒子出生时的速度单位是米/秒不是发射数量。发射数量由Capacity和Life共同决定。Capacity是最大存活粒子数Life是每个粒子的存活时间存活时间内粒子如果没被销毁就会一直占着缓冲池。下面是个很基础的发射控制脚本我在交互装置项目里经常这样写using UnityEngine; using Obi; public class SimpleObiController : MonoBehaviour { public ObiEmitter emitter; // 在Inspector里拖入 public float emitSpeed 5f; // 初始发射速度 public KeyCode stopKey KeyCode.Space; void Update() { // 按住空格停止发射松开继续 if (Input.GetKey(stopKey)) { emitter.speed 0f; } else { emitter.speed emitSpeed; } // 按B键切换流体颜色需要材质支持 if (Input.GetKeyDown(KeyCode.B)) { var renderer emitter.GetComponentRenderer(); if (renderer ! null) { renderer.material.color new Color(0.2f, 0.6f, 1f, 0.8f); } } } }逻辑说明ObiEmitter.speed设0发射器就不再产生新粒子但已生成的粒子不会自动消失它们继续受力和碰撞直到超过Life时间。这个脚本适合做阀门式的液体控制。参数说明emitSpeed的数值范围一般在0到20之间太大容易穿透障碍物因为粒子在一个物理帧内的位移超过了碰撞体厚度。Capacity默认值在10000以内比较安全如果业务里需要长时间持续发射记得把Capacity调大一些否则发射器满了会停止运行。注意改颜色这里用的是Renderer.material.color它影响的是整个发射器对象的材质如果你使用多个子渲染器需要遍历一下。4.3 用鼠标拖拽液体把粒子变成可交互的“玩具”在桌面交互场景中常见做法是让流体跟随鼠标或触控点运动。这里有一个方案上的选择直接操作粒子速度或者用一个虚拟碰撞体去推流体。直接操作粒子速度需要拿ObiEmitter的粒子数组还要同步访问ObiSolver的物理状态每帧遍历所有粒子移动端撑不住。所以我更推荐用虚拟碰撞体去推流体因为Obi的碰撞检测本身就是并行的几乎不占额外CPU。下面是一段简化的拖拽脚本using UnityEngine; using Obi; public class DragFluid : MonoBehaviour { public ObiEmitter emitter; public float pushStrength 2f; private ObiCollider cursorCollider; private GameObject cursorObject; void Start() { // 创建一个球体碰撞体作为鼠标光标 cursorObject GameObject.CreatePrimitive(PrimitiveType.Sphere); cursorObject.name CursorCollider; cursorObject.transform.localScale Vector3.one * 0.2f; Destroy(cursorObject.GetComponentCollider()); // 移除原生Collider cursorCollider cursorObject.AddComponentObiCollider(); // 把碰撞体放到发射器所在的物理层避免干扰 cursorObject.layer emitter.gameObject.layer; } void Update() { Vector3 worldPos; if (GetMouseWorldPosition(out worldPos)) { cursorObject.transform.position worldPos; } } bool GetMouseWorldPosition(out Vector3 pos) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100f)) { pos hit.point; return pos; } pos Vector3.zero; return false; } }逻辑说明这里用一个小球作为“鼠标指针”在Update里不停更新位置流体粒子因为它有ObiCollider而会被推开。注意我销毁了原生SphereCollider因为ObiCollider只需要挂载不需要和PhysX碰撞体共存否则会造成双重碰撞反馈。参数说明pushStrength字段本身是预留的如果你想加强显式的推挤力可以改成在OnTriggerEnter里对进入的粒子施加速度但实际测试下来用虚拟碰撞体推动的方式更稳定没有穿透。鼠标指针球体的大小决定推挤范围0.2米对应一个中等手掌大小的效果。如果你的场景里还有其他物理控件记得把cursorObject放在专门的Layer避免和UI射线交互冲突。5. 踩坑与排查Obi Fluid仿真翻车最常见的五个问题5.1 粒子从容器底部直接漏下去现象打开Demo场景粒子喷出来但碰到容器底部后直接穿过到地面而不是聚积起来。原因容器使用了Unity自带的Box Collider没有挂ObiCollider组件。Obi Fluid有自己的一套碰撞检测不识别原生Collider。解决给所有承载流体的物体添加ObiCollider组件。如果是Demo场景已经带了ObiCollider检查容器物体的Scale是否是负数。负缩放会干扰Obi的边界检测导致粒子反向穿越。我遇到过容器整体Scale是-1来做镜像结果粒子全部透过清零后正常。5.2 粒子像爆炸一样四处飞散现象液体刚接触地面就弹起很高或者粒子瞬间飞离场景。原因求解器迭代次数过低加上碰撞反弹恢复系数太高。常见误区是觉得“水有弹性”于是把Restitution调成0.5以上结果水变成了弹力球。解决先把迭代次数调到4以上再把Restitution调到0.1左右。如果还炸把Surface Tension调低一档同时检查Particle Collision是否开启。粒子之间的穿透也会使求解器算出极端压力引发爆炸。还有一个玄学点如果场景中有多个Solver要检查粒子是否被错误分配到了两个Solver的求解域里这会导致力重复计算。5.3 改了蓝图参数运行后没有变化现象把Viscosity从0.05改到1粒子还是像水一样哗哗流完全不黏。原因蓝图是资源文件场景里的ObiEmitter引用的是蓝图实例。如果没有选中蓝图资源直接改场景里的参数或者改完蓝图没保存运行时用的是缓存的旧数据。解决修改时先选中Project窗口里的蓝图资产在Inspector中改参数然后CtrlS保存。改完后退出Play模式重新进入生效。这里要特别注意运行时动态修改蓝图是不行的Obi的Blueprint是只读资产。如果你想在运行时切换液体类型需要准备多个ObiEmitter分别设置不同的Blueprint然后通过切换GameObject的active来控制显示哪一个。5.4 移动端帧率只有个位数现象同样的场景在PC上有60帧打包到手机上直接卡成PPT。原因粒子数太多加上移动端GPU的Compute Shader能力弱。很多交互项目喜欢拿PC上漂亮的粒子数直接打包。解决先用Profiler确认时间花在ObiSolver.Update还是ObiParticleRenderer。前者是求解器瓶颈需要减粒子数或降迭代次数后者是渲染瓶颈需要改用Instanced粒子材质关闭屏幕空间反射。在Android端我一般会把粒子半径调大以降低粒子数同时把Renderer的抗锯齿关掉。别忘了把帧率目标设成和项目一致不要默认的垂直同步。5.5 粒子数过多导致内存溢出闪退现象运行几秒后Unity Editor直接崩溃或者手机闪退。原因发射器容量设得过大而粒子在存活周期内没有自动回收。这个问题最隐蔽的是当你把一个发射器的Capacity调到100万后即使不发射Obi上层的粒子数组也会预留巨大内存。解决把Capacity控制在需要的1.2倍以内。如果你的发射器是循环喷泉最好估算一下同时存活的粒子峰值发射速度乘以Life时间再乘以发射面积。如果超出预期减少Life或降低发射速度。这里给一个简单可靠的回收方案void CloseEmitterAfter(float seconds) { StartCoroutine(StopLater(seconds)); } System.Collections.IEnumerator StopLater(float s) { yield return new WaitForSeconds(s); GetComponentObiEmitter().speed 0f; }逻辑说明这段代码在指定时间后把发射速度归零。注意关闭发射不会清空已发射的粒子如果场景还在粒子会自然消融。如果想让已有粒子也立即消失需要调用emitter.ClearParticles()但这个操作比较重建议只在切换场景时用。还有一个注意事项ClearParticles()会触发GC如果你每帧调用会卡死。所以尽量用Life机制让粒子自然消亡而不是手动清空。6. 进阶技巧用音频频谱驱动流体发射与颜色变化做互动装置或音乐可视化时让流体跟随音乐节奏变化是非常讨巧的效果。我试过几种方案最稳的是从Unity的AudioSource里取出实时频谱再映射到ObiEmitter的发射速度和粒子颜色而不是直接改物理参数。改物理参数会破坏蓝图的统一性而且响应跟不上音乐节拍。思路是这样的新建一个AudioSource设置一个循环的BGM或环境音然后挂一个脚本每100毫秒取一次GetSpectrumData算出低频、中频的能量值。低频能量用来控制发射速度中频能量用来控制颜色明度。下面是核心脚本片段using UnityEngine; using Obi; [RequireComponent(typeof(AudioSource))] public class AudioDrivenFluid : MonoBehaviour { public ObiEmitter emitter; public float lowSensitivity 5f; public float colorChangeSpeed 2f; private float[] spectrum new float[64]; private Renderer fluidRenderer; void Start() { fluidRenderer emitter.GetComponentRenderer(); } void Update() { GetComponentAudioSource().GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris); float lowBand 0f; for (int i 0; i 16; i) { lowBand spectrum[i]; } lowBand / 16f; emitter.speed Mathf.Clamp(lowBand * lowSensitivity, 0f, 20f); if (fluidRenderer ! null) { float hue Mathf.Sin(Time.time * colorChangeSpeed) * 0.2f 0.6f; fluidRenderer.material.color Color.HSVToRGB(hue, 0.8f, 0.5f lowBand * 2f); } } }参数说明GetSpectrumData的频谱长度是64前16个采样点对应低频适合提取鼓点。lowSensitivity需要按你音频的音量调一般3到8之间。颜色变化用的Hue在0.6附近对应蓝色系避免溢出刺眼。Color.HSVToRGB可以保证颜色亮度不刺眼比直接调RGB线性插值更自然。这里有个很值得注意的坑Obi Fluid的粒子渲染是CPU或GPU实例化直接改material.color在编辑器里能生效但打包后可能会被SRP Batcher吞掉。更稳的做法是改Shader里暴露的参数比如在材质里建一个_FluidColor属性然后在脚本里用material.SetColor(_FluidColor, newColor)去设置而不是直接赋给material.color。另外如果你的AudioSource没有设置循环频谱会很快变成静音发射器自动归零。做展览时记得加一首循环音乐或者明确写一段节拍生成器。从那以后我每次做这种交互流体项目都会强制先跑一轮性能压测固定视角、固定粒子数把音频脚本开启10分钟看有没有内存增长和掉帧曲线。只有通过这个测试我才敢把版本交给前端去集成。这个习惯帮我避开了很多发布会当场翻车的尴尬。希望今天这篇笔记也能帮到你。本文还有配套的精品资源点击获取
返回列表