在Unity中实现基于粒子的水模拟(二:开始着色)


文章目录

  • 在Unity中实现基于粒子的水模拟(二:开始着色)
  • 前言
  • 一、生成顶点
  • 二、偏移模拟
    • 1.接收细分着色器输出的顶点
    • 2.根据数据调用对应的处理方法
    • 3.曲线拟合
    • 4.碰撞后的模拟
  • 三、着色
    • 1.片元着色器输入
    • 2.生成宽度数据
    • 3.生成法线和深度数据
  • 总结
    • 1.一点小问题
    • 2.补充

前言

笔者最近在研究Unity的可编程渲染管线,参考的文章地址,之后的项目应该都会基于该渲染管线进行拓展。
同时本文是基于这篇文章的Unity实现,只是一种实现的参考。
由于粒子进行模拟时会有恐怖的Overdraw,所以其实在游戏中实时运行还是有点奢侈了,但是如果只是作为游戏中固定的流体模拟,不需要场景改变效果的话还是可以通过定制来实现很好的效果的。

不过本文还是实时模拟的,在一些细节上并没有实现的很好,比如液体碰撞到物体后的效果实现,因为更物理的粒子实现太奢侈了,因此只是简单的实现,想要更好的效果就自己定制吧。

同时这里将物理帧的数据刷新换为了实时帧,让液体喷出时更连续,同时将开头的循环删去,换为了不能刷新就等待,而不是循环一遍,因为这个循环会导致帧数变得很不稳定,更新后的效果:

在自定义渲染管线中实现喷水效果


一、生成顶点

粒子的生成是通过曲面细分生成的顶点来生成的,也就是在我的这篇文章生成粒子的格式生成的,同时将生成粒子的顶点部分全部放到了一个文件中处理,让整体更加模块化,不像一开始将整个流程放在了一个文件中。

首先在曲面细分的结构体中要有我们传入的所有数据,同时为了让输入与输出区分开定义了两个结构体,但是实际上这两个结构体的数据是一致的,因为需要传递给几何着色器,由几何着色器进行数据计算。

这里提一下,之前搜API时看少了,因为Unity只提供了设置float2格式的uv坐标,但是实际上是可以支持float4的坐标的,需要的话的可以换一下设置数据的方法,更充分的利用每一个数据。

struct TessVertex_All{float4 vertex : POSITION;float4 color : COLOR;float3 normal : NORMAL;float4 tangent : TANGENT;float2 uv0 : TEXCOORD0;float2 uv1 : TEXCOORD1;float2 uv2 : TEXCOORD2;float2 uv3 : TEXCOORD3;float2 uv4 : TEXCOORD4;float2 uv5 : TEXCOORD5;float2 uv6 : TEXCOORD6;
};struct TessOutput_All{float4 vertex : Var_POSITION;float4 color : Var_COLOR;float3 normal : Var_NORMAL;float4 tangent : Var_TANGENT;float2 uv0 : Var_TEXCOORD0;float2 uv1 : Var_TEXCOORD1;float2 uv2 : Var_TEXCOORD2;float2 uv3 : Var_TEXCOORD3;float2 uv4 : Var_TEXCOORD4;float2 uv5 : Var_TEXCOORD5;float2 uv6 : Var_TEXCOORD6;
};

然后就是曲面细分的标准格式了,还是那套流程,不过需要注意的是在SRP中的曲面细分支持检测的宏需要自己定义,为了方便我直接删除了,毕竟大部分机器都能够支持了。

//顶点着色器的输入值,直接传递不进行操作
void tessVertAll (inout TessVertex_All v){}//细分参数控制着色器,细分的前置准备
OutputPatchConstant hullconst(InputPatch<TessVertex_All, 3>v){OutputPatchConstant o = (OutputPatchConstant)0;float size = _TessDegree;//获得三个顶点的细分距离值float4 ts = float4(size, size, size, size);//本质上下面的赋值操作是对细分三角形的三条边以及里面细分程度的控制//这个值本质上是一个int值,0就是不细分,每多1细分多一层//控制边缘的细分程度,这个边缘程度的值不是我们用的,而是给Tessllation进行细分控制用的o.edge[0] = ts.x;o.edge[1] = ts.y;o.edge[2] = ts.z;//内部的细分程度o.inside = ts.w;return o;
}[domain("tri")]    //输入图元的是一个三角形
//确定分割方式
[partitioning("fractional_odd")]
//定义图元朝向,一般用这个即可,用切线为根据
[outputtopology("triangle_cw")]
//定义补丁的函数名,也就是我们上面的函数,hull函数的返回值会传到这个函数中,然后进行曲面细分
[patchconstantfunc("hullconst")]
//定义输出图元是一个三角形,和上面对应
[outputcontrolpoints(3)]
TessOutput_All hull (InputPatch<TessVertex_All, 3> v, uint id : SV_OutputControlPointID){return v[id];
}[domain("tri")]
TessOutput_All domain_All (OutputPatchConstant tessFactors, const OutputPatch<TessOutput_All, 3> vi, float3 bary : SV_DomainLocation){TessOutput_All v = (TessOutput_All)0;v.vertex = vi[0].vertex * bary.x + vi[1].vertex*bary.y + vi[2].vertex * bary.z;v.normal = vi[0].normal * bary.x + vi[1].normal*bary.y + vi[2].normal * bary.z;v.tangent = vi[0].tangent * bary.x + vi[1].tangent*bary.y + vi[2].tangent * bary.z;v.color = vi[0].color * bary.x + vi[1].color*bary.y + vi[2].color * bary.z;v.uv0 = vi[0].uv0 * bary.x + vi[1].uv0*bary.y + vi[2].uv0 * bary.z;v.uv1 = vi[0].uv1 * bary.x + vi[1].uv1*bary.y + vi[2].uv1 * bary.z;v.uv2 = vi[0].uv2 * bary.x + vi[1].uv2*bary.y + vi[2].uv2 * bary.z;v.uv3 = vi[0].uv3 * bary.x + vi[1].uv3*bary.y + vi[2].uv3 * bary.z;v.uv4 = vi[0].uv4 * bary.x + vi[1].uv4*bary.y + vi[2].uv4 * bary.z;v.uv5 = vi[0].uv5 * bary.x + vi[1].uv5*bary.y + vi[2].uv5 * bary.z;v.uv6 = vi[0].uv6 * bary.x + vi[1].uv6*bary.y + vi[2].uv6 * bary.z;return v;
}

二、偏移模拟

1.接收细分着色器输出的顶点

代码如下(示例):

[maxvertexcount(30)]
void geom(triangle TessOutput_All IN[3], inout TriangleStream<FragInput> tristream)
{LoadWater(IN[0], tristream);LoadWater(IN[1], tristream);LoadWater(IN[2], tristream);
}

LoadWater函数是用来确定该顶点状态的函数,用来进行数据准备以及调用对应的处理函数。

2.根据数据调用对应的处理方法

LoadWater是判断该顶点的运动阶段是碰撞前(曲线阶段)还是碰撞后(自定义偏移阶段),因为不同阶段对数据处理的方式不同,因此采用不同的处理方式。

先确定顶点的启动时间以及顶点的阶段,在同一批的粒子(一个三角面)不是同一时间输出的,而是有一个输出的时间范围,因此需要确定该顶点是否在输出时间。

//这批粒子的启动时间,IN.uv6.x是移动时间
float beginTime = IN.tangent.w - _OutTime - _OffsetTime - IN.uv6.x;
//这个顶点的发出时间,ramdom是一个根据顶点随机的float4数据
float outTime = _OutTime * ramdom.w + beginTime;
//不在顶点发出时间,不进行射出
if(_Time.y < outTime  ) return;

接着根据数据判断一下属于哪个阶段以及数据情况,调用对应的处理方法。

 //偏移阶段,也就是碰撞后的一段时间if(partiTime >= 1){float3 end = (float3)0;if(step(0.5, IN.color.x))end = float3(IN.uv3.xy, IN.uv4.x);elseend = IN.tangent.xyz;Offset(IN, (_Time.y - outTime - IN.uv6.x) / _OffsetTime,tristream, ramdom, end );return;}if(step(0.5, IN.color.x)){    //第一条射线射中OnePointEnd(IN, partiTime, tristream);}else{    //第二条射线射中TwoPointEnd(IN, partiTime, tristream);}

3.曲线拟合

通过设置的数据进行该顶点位于的位置获取,也就是通过贝塞尔曲线进行曲线确定该时间上顶点应该位于的世界坐标。拟合结束后输出到顶点生成平面的方法(Move_outOnePoint)。

//当第一条射线就碰到物体时执行的方法
void OnePointEnd(TessOutput_All IN, float moveTime, inout TriangleStream<FragInput> tristream){float3 begin = float3(IN.uv0.xy, IN.uv1.x);float3 center = float3(IN.uv2.xy, IN.uv1.y);float3 end = float3(IN.uv3.xy, IN.uv4.x);IN.vertex.xyz = Get3PointBezier(begin, center, end, moveTime);Move_outOnePoint(tristream, IN, moveTime, end);
}//第二条射线碰撞到的情况
void TwoPointEnd(TessOutput_All IN, float moveTime, inout TriangleStream<FragInput> tristream){float3 begin = float3(IN.uv0.xy, IN.uv1.x);float3 center = float3(IN.uv2.xy, IN.uv1.y);float3 end = float3(IN.uv3.xy, IN.uv4.x);float3 target1 = Get3PointBezier(begin, center, end, moveTime);begin = end;center = float3(IN.uv5.xy, IN.uv4.y);end = IN.tangent.xyz;float3 target2 = Get3PointBezier(begin, center, end, moveTime);IN.vertex.xyz = target1 + (target2 - target1) * moveTime;Move_outOnePoint(tristream, IN, moveTime, end);
}

4.碰撞后的模拟

碰撞后的模拟目前实现的很随便,也就是这篇文章的实现的移动方式,内容很少,因此这个部分是肯定需要根据项目重新设置的。

//偏移阶段
void Offset(TessOutput_All IN, float offsetTime, inout TriangleStream<FragInput> tristream, float4 random, float3 begin){if(offsetTime > 1 || offsetTime < 0) return;float3 dir0 = lerp(_VerticalStart, _VerticalEnd, random.xyz);float3 normal = IN.normal;//确定旋转矩阵float cosVal = dot(normalize( normal ), float3(0, 1, 0));float sinVal = sqrt(1 - cosVal * cosVal);float3x3 xyMatrix = float3x3(-cosVal, sinVal, 0,-sinVal, -cosVal, 0,0, 0, 1);float3x3 yzMatrix = float3x3(1, 0, 0,0, -cosVal, sinVal,0, -sinVal, -cosVal);float3x3 xzMatrix = float3x3(-cosVal, 0, -sinVal,0, 1, 0,sinVal, 0, -cosVal);float3 targetDir = mul(xzMatrix, mul( yzMatrix, mul(xyMatrix, dir0) ));IN.vertex.xyz = begin + targetDir * offsetTime;Offset_outOnePoint(tristream, IN, offsetTime);
}

三、着色

1.片元着色器输入

首先描述一下片源着色器的输入结构体,因为这个输出的处理方式和这篇文章是一样的,因此只描述一下输入的数据。

struct FragInput{
//这个粒子平面的uv坐标,和particle system的粒子uv分布一样float2 uv : TEXCOORD0;  float4 pos : SV_POSITION;//目前没有用到该数据,因此目前就是粒子总时间float time : TEXCOORD2;//x为0时是曲线阶段,1时为碰撞后//zyw存储了这个粒子的球面中心float4 otherDate : TEXCOORD3;//存储世界空间位置float3 worldPos : TEXCOORD4;float4 otherDate2 : TEXCOORD5;  //预留数据
};

2.生成宽度数据

生成宽度数据的格式很简单,直接根据纹理颜色值返回就行了,我用的纹理是一张带透明通道的圆形图,采集纹理颜色后根据对应的阶段来乘以其的透明度就行了。

    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv) * _Color;#ifdef _CURVE_ALPHAif(i.otherDate.x < 0.1){col *= saturate( LoadCurveTime( i.time.x, _MoveAlphaPointCount, _MoveAlphaPointArray ) );}else {col *= saturate( LoadCurveTime( i.time.x, _OffsetAlphaPointCount, _OffsetAlphaPointArray ) );}#endif

实际上宽度数据最重要的是纹理的混合模式,因为要让数据叠加,因此要使用的混合模式是One One,不过为了更好的定义,可以通过设置混合模式为选项来控制混合效果。

[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1

叠加模式的宽度图:

3.生成法线和深度数据

由于在渲染深度时需要深度写入,因此我顺便将法线数据也一同写入了。
在粒子中生成法线的方式很简单,因为首先我们需要获得当这个粒子为球时的球心,其实就是生成粒子的根据点沿摄像机方向原理球的半径。

//worldVer是根据顶点的世界坐标,paritcleLen为半径
float3 sphereCenter = worldVer - UNITY_MATRIX_V[2].xyz * paritcleLen;

通过圆心的位置与该像素的世界坐标的差来得到法线数据,不过直接这样会在圆的外部也有数据(因为粒子是一个平面),因此需要根据透明度剔除。

float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
#ifdef _CURVE_ALPHAif(i.otherDate.x < 0.1){col *= saturate( LoadCurveTime( i.time.x, _MoveAlphaPointCount, _MoveAlphaPointArray ) );}else col *= saturate( LoadCurveTime( i.time.x, _OffsetAlphaPointCount, _OffsetAlphaPointArray ) );
#endif
clip(col.a - 0.5);       //剔除的位置float3 normal = normalize(i.worldPos - i.otherDate.yzw);
//由于纹理不能存储负数,需要进行数据映射
return float4(normal * 0.5 + 0.5, 1);

生成的法线图,深度图因为精度问题,就不显示了,反正都是全黑


总结

1.一点小问题

到此就完成了3个纹理的渲染了,这里需要说明一下,我这里的渲染的调用是通过可编程渲染管线调用的,因为其可以让数据渲染到我想要的位置,但是如果是在默认管线中是不能这么操作的。
因为默认管线渲染只能通过指定Camera的TargetTexture来渲染,通过指定两个摄像机渲染,可以渲染出这两张图片,不过这里有一个小问题,就是我没有找到Unity中传递纹理默认的深度数据的方式,导致一开始还用了一个摄像机来渲染深度。

2.补充

实际上我这里的粒子模拟是有很大问题的,因为两个阶段导致有交叉问题,比如边缘的法线顶替了曲线的法线,但是实际上边缘的法线的水不怎么厚,因此我觉得这种模式的粒子模拟方式不好,不过如果是通过粒子来模拟水流之类的效果应该会好很多,成本也低。

同时可以因为水有厚度值,如果是模拟牛奶之类的使用BTDF效果模拟也会很不错。

在Unity中实现基于粒子的水模拟(二:开始着色)相关推荐

  1. 在Unity中实现基于粒子的水模拟(三:混合屏幕)

    在Unity中实现基于粒子的水模拟(三:混合屏幕) 文章目录 在Unity中实现基于粒子的水模拟(三:混合屏幕) 前言 一.着色算法介绍 1.折射 2.反射 二.准备纹理 1.获取纹理 2.模糊纹理 ...

  2. 在Unity中实现基于粒子的水模拟

    曲面细分进行水模拟(一:物理模拟) 文章目录 曲面细分进行水模拟(一:物理模拟) 前言 一.曲线模拟的原理介绍 二.代码计算终点 1.代码原理介绍 2.第一条射线计算 3.第二条射线计算 4. 分配数 ...

  3. 在Unity中创建基于Node节点的编辑器 (二) 窗口序列化

    孙广东  2018.5.13 csdn 的产品 , 真垃圾, 不想吐槽了, 文章保存就丢!     没办法  .    怎么不满意, 还是得继续用, 哎~~~ 第二部分 在Unity中序列化基于节点的 ...

  4. 在Unity中创建基于Node节点的编辑器 (一)

    孙广东   2018.5.13 Unity  AssetStore中关于Node节点 编辑器相关的插件可是数不胜数, 状态机,行为树,Shader 可视化等等. Unity自己也有 Animator的 ...

  5. Unity中UGUI之Canvas属性解读版本二

    Canvas Render Modes(渲染模式) 1.在screen空间中渲染 2.在world空间中渲染 Screen Space-Overlay 在这个渲染模式中,UI元素将在场景的上面.如果场 ...

  6. 学习在Unity中创建一个动作RPG游戏

    游戏开发变得简单.使用Unity学习C#并创建您自己的动作角色扮演游戏! 你会学到什么 学习C#,一种现代通用的编程语言. 了解Unity中2D发展的能力. 发展强大的和可移植的解决问题的技能. 了解 ...

  7. Unity中自制Animation+播放完毕相应事件

    一.目的 1.想知道:Unity中自制Animation+播放完毕相应事件 二.参考 1.Unity动画播放结束事件 https://blog.csdn.net/qq_34244317/article ...

  8. Unity中的灯光和渲染

    一:Unity中的灯光 --Directional Light:模拟太阳光.它与位置无关,是平行光,可以调整旋转角度模拟昼夜 --Spot Light:模拟车灯.手电筒的光.舞台灯光 --Point ...

  9. Unreal Engine 4 基于网格的水面模拟实现

    http://blog.csdn.net/shangguanwaner/article/details/51862644 Unreal Engine 4 水面模拟实现 一般游戏里水面的模拟都是实用动态 ...

最新文章

  1. 【NLP】博士笔记 | 深入理解深度学习语义分割
  2. 人人都应该掌握的9种数据分析思维
  3. python笔记本-如何用Python在笔记本电脑上分析100GB数据(下)
  4. pyautogui 打包 运行 窗口_试试动态窗口管理器 dwm 吧
  5. 2清空所有表_拉链表(二)
  6. linux 修改块大小,linux 查看及修改os系统块的大小
  7. 我的世界末日求生系列服务器,末日生存 少年pi
  8. MessageDigest简介
  9. matlab 0x1表示什么意思,(x ^ 0x1)!= 0是什么意思?(What does (x ^ 0x1) != 0 mean?)
  10. Matlab中的逻辑运算与,||与|的区别
  11. Ubuntu Thinkphp page not found
  12. mysql5.7.18压缩包下载_MySQL5.6.30 升级到MySQL5.7.18
  13. java代表预设一个SQL_java-io基础-3-压缩和解压
  14. 从零开始学习Java设计模式 | 设计模式入门篇:教程导读
  15. CMT 注册——Google Scholar Id,Semantic Scholar Id,和 DBLP Id
  16. 泛微oa系统什么框架_泛微OA系统怎么样?与其他OA相比呢?
  17. 2018年阿里安全工程师面试
  18. 职场智慧:君子应处木雁之间,当有龙蛇之变
  19. 【历史上的今天】12 月 8 日:D 语言发布;“复制粘贴”的发明者逝世;人人网成立
  20. Oracle服务与配置

热门文章

  1. 如何打造自己的PoC框架-Pocsuite3-框架篇
  2. 计算机网络第七版(谢希仁)第三章——数据链路层课后习题答案
  3. minio服务器在win10的上传与下载,以及修改头像Minio速看免费本地文件服务器
  4. linux activemq 打印日志,Log4j.xml配置日志按级别过滤并将指定级别的日志发送到ActiveMQ...
  5. [HTML] HTML简单实现网络测速
  6. shell编程常用命令总结(二)
  7. Oracle Database 9i/10g/11g编程艺术:深入数据库体系结构
  8. 老鹰:我要抓走倒数第K个小鸡
  9. 2022年江西省建筑三类人员(企业主要负责人A证)练习题及答案
  10. 2019年第十届蓝桥杯c/c++B组国赛决赛真题题目