在Unity中实现基于粒子的水模拟(二:开始着色)
在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.生成法線和深度數據
由于在渲染深度時需要深度寫入,因此我順便將法線數據也一同寫入了。
在粒子中生成法線的方式很簡單,因為首先我們需要獲得當這個粒子為球時的球心,其實就是生成粒子的根據點沿攝像機方向原理球的半徑。
通過圓心的位置與該像素的世界坐標的差來得到法線數據,不過直接這樣會在圓的外部也有數據(因為粒子是一個平面),因此需要根據透明度剔除。
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中实现基于粒子的水模拟(二:开始着色)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 二级考试c语言中 星号与字母 题型总结,
- 下一篇: App读取短信实现