mypro334 发表于 2022-6-3 19:32

Unity实现基于粒子的水模拟(一:物理模拟)

前言

之前花费了不少时间编写了一套通过GPU进行粒子系统的模拟,效率很不错,因此觉得只用来制作雾效太浪费了,于是打算再多实现点什么东西。正好[这篇文章](Nae Zhu:液体渲染:一种屏幕空间方法)的液体成色的原理给了我启发,于是打算添加一点简单的水枪效果模拟,没有很真实的物理效果,只是使用射线获取落点然后数据进行贝塞尔曲线拟合。
这并非是真正的液体模拟,只是制作一个简单的水枪效果,但是速度够快,主要的损耗在射线上,如果是静态的模拟的话甚至可以直接舍去这一部分,只进行水着色处理,这样损耗就更少了。

效果请看视频



https://www.zhihu.com/video/1508747466299711488
由于这次的模拟效果涉及的内容过多,因此我打算分为几篇文章来写,这是第一篇,使用代码进行一些预运算,准备数据传递给材质进行模拟。

一、曲线模拟的原理介绍

由于Unity并不存在曲线检测,我们不能真正的进行曲线的碰撞检测(印象中射线检测算法确实只有直线的检测,毕竟是包围盒切割,曲线的话包围盒都不好确定了),但是我们可以首先通过公式算出终点,然后从起点到终点射一条射线,碰撞到的点就是曲线的终点,这样确实不是真实效果,但是对于简单的模拟还是足够的。
看图理解:



射线路径没有遮挡物



射线路径有遮挡物

光看图可能会觉得有遮挡和没遮挡差距很大,但由于只是曲线的终点有一定的差距,只要射线距离不要太大,看起来也不会有很大的问题,但是射线大小一设大就有比较明显的差距,还是有点影响的,但先这样吧。

一条射线只能确定上方会不会碰撞到物体,水是会下落的,因此我们要确定下面有没有物体。完整流程如下:


当然,实际效果可比我手画的好多了,因为是一个贝塞尔曲线,至少曲线的效果还是有的,只是这是射线检测,不能判断到曲线过程中会射中的点,因此可能会在曲线中依旧会有穿模现象,不过只要射线大小不要太大,穿模是不会经常出现的。

二、代码计算终点

1.代码原理介绍

根据原理我们发现至少要有2条射线,但是实际上由于水枪的出发点不可能正好在地面上,因此需要知道地面的高度,因此在最坏的情况下我们需要发出三条射线,如果没有遮挡,这就是正常情况,因此为了这个物理模拟,确实有一点奢侈。

在第一条射线中,目标是确定是否有上方遮挡物,因此可以直接根据计算出来的终点往上射,看看是否有碰撞到的点。不过第一条射线可以舍去,比如当初始方向朝下时,不可能向上飞,因此可以直接舍去第一条射线,直接从第二条射线开始计算,此时起始点就是自身的坐标。

第二条射线就是进行下降的计算,需要注意的时由于我是默认地面是水平的,没有明显的起伏,高度一直都是和获取到的地面高度一致,如果不一致的话由于射线是一条直线,下落曲线就会被拉的很长,曲线效果可能会有一定的影响,同时时间直接按照我的写法也会有一定的出入,但是时间方面只要在经过一次计算就可以解决,主要问题还是曲线。

第三条射线是为第二条射线的地面位置做准备,为了能够确定第二条射线的落点,需要预先获取地面位置,因此需要一条射线直接向下射,获取到地面的Y轴,根据这个进行落点计算。

2.第一条射线计算


代码如下(示例):
//用模型的前方作为我们的水枪起始方向,rayDis是射线强度,
//upDir 计算出来的就是液体的速度
Vector3 upDir = transform.forward * rayDis;
Vector3 upPos = transform.position;
Vector3 veTemp = Vector3.zero;
float upTime = 0;

//向上时执行上抛,否则直接向下确定位置
if (transform.forward.y >= 0)
{
        //本质上下面几行代码就是一个自由落体,获取到最大高度
    upTime = upDir.y / 9.8f;
    upDir.y = 0;
    //确定高度位置
    upPos.y += 0.5f * 9.8f * upTime * upTime;
    //确定最后终点
    upPos += upDir * upTime;
    //确定距离差
    veTemp = upPos - transform.position;
    //第一条线射中目标,检查是否上方有东西阻挡
    if (Physics.Raycast(transform.position, veTemp, out raycastHit, veTemp.magnitude, layer))
    {
            //第一条射线射中数据传递的方法,后面介绍
      OneRayHit(raycastHit, raycastHit.distance, upTime);
      Debug.DrawLine(transform.position, raycastHit.point, 403 Forbidden);
      return;
    }
}

3.第二条射线计算

第二条射线需要预先准备地面高度,因此首先发射一条射线到地面。
//默认底部高度
float buttonY = transform.position.y - 1;

if (Physics.Raycast(transform.position, Vector3.down, out raycastHit, layer))
{
   buttonY = raycastHit.point.y;
}


确定了高度后就可以检查第二个落点了,代码如下

float s = upPos.y - buttonY;

//就是简单的解方程,指导s和v计算t的方程
//这里计算只用到了Y值,根据的是Y值差确定时间
float downTime = Mathf.Sqrt(
    2 * (0.5f * 9.8f * Mathf.Pow(upDir.y / 9.8f, 2) + s) / 9.8f
    ) - Mathf.Abs(upDir.y) / 9.8f;
upDir.y = 0;
Vector3 downPos = downTime * upDir + transform.position;
downPos.y = buttonY;


//第二条射线默认无限距离,往尽头射,因为终点很可能不在同一高度
if (Physics.Raycast(upPos, downPos - upPos, out raycastHit, layer))
{
        //第二条射线碰撞到物体的情况,要有好的物理模拟效果,
        //需要再进行一次时间计算,因为还有开方,计算量大,我舍去了
    TwoRayHit(raycastHit, upPos, veTemp.sqrMagnitude, transform.forward, downTime + upTime);
    Debug.DrawLine(upPos, raycastHit.point, 403 Forbidden);

    return;
}

//第二条也没有中,就在空中结束
//赋值默认碰撞点
raycastHit.point = downPos;
raycastHit.normal = Vector3.up;
TwoRayHit(raycastHit, upPos, veTemp.sqrMagnitude, transform.forward, downTime + upTime);

Debug.DrawLine(upPos, raycastHit.point, Color.white);
有了碰撞点后就需要将数据传递给材质进行着色了,为了有足够好的物理模拟效果,我们需要保证值之间能够独立,不受其他值影响,因此直接设置材质的变量是一定不行的。
要设置独立存在的数据最好的位置是什么,自然是模型的顶点数据啊,uv、normal、tangent、color都能作为我们的数据传递位置,数据之间也不会相互影响。

首先我们需要确定要传递什么数据,首先,起始点和终点是一定需要的,为了有液体射出去的效果,我们需要存储时间,为了能够拟合出贝塞尔曲线,需要存储两个射线的中间位置,为了保证粒子系统效果的生成,需要有固定不变的定值,因此模型空间不能变。所以数据分配结果如下:

[*]首先顶点数据依旧用来存储随机数,这个数据是固定不变的, 第一条射线的起点存储在uv0和uv1(x)中,即begin=(uv0.xy,   uv1.x)
[*]第一条射线的贝塞尔曲线 中点 存储在uv1(y)和uv2中,其中center=(uv2.xy, uv1.y)
[*]第一条射线的终点[也是第二条射线的起点]在uv3和uv4(x)中,即end=(uv3.xy, uv4.x)
[*]第二条射线的贝塞尔曲线中点存储在uv4(y)和uv5中,其中center=(uv5.xy, uv4.y)
[*]第二条射线的终点存储在tangent中,其中end=(Corporate governance and insider trading resources) 射线的最终点的法线存储在normal中,就是normal   = normal
[*]这个点的结束时间存储在tangent.w中,这个也是刷新的根据时间,确定该粒子是否可用
[*]color存储了这批点的射中类型,x为1时就是第一条射线射中,y为1就是第二条射线射中
[*]为了物理模拟,移动时间设在uv6的x中,保证这个模液体移动时间是可变的
4. 分配数据准备

要保证数据能够正常分配,因此需要代码生成顶点,由于粒子效果的模拟时会涉及曲面细分,如果一个面的数据差距很大时,很可能导致有一些粒子值差距很大,导致一闪一闪的效果,因此建议使用一次赋值一个三角面。

因此顶点生成算法很简单,根据设置的顶点数量生成顶点,这些顶点看起来都是一个三角面。

代码如下

/// <summary>
/// 用来初始化这个生成的顶点,为了画出较好的贝塞尔曲线,
/// 且保证每一边都是贝塞尔曲线,打算将三个点的数据传入其中
/// 首先顶点数据依旧用来存储随机数,这个数据是固定不变的,
/// 第一条射线的起点存储在uv0和uv1(x)中,即begin=(uv0.xy, uv1.x)
/// 第一条射线的贝塞尔曲线 中点 存储在uv1(y)和uv2中,其中center=(uv2.xy, uv1.y)
/// 第一条射线的终点[也是第二条射线的起点]在uv3和uv4(x)中,即end=(uv3.xy, uv4.x)
/// 第二条射线的贝塞尔曲线 中点 存储在uv4(y)和uv5中,其中center=(uv5.xy, uv4.y)
/// 第二条射线的终点存储在tangent中,其中end=(Corporate governance and insider trading resources)
/// 射线的最终点的法线存储在normal中,就是normal = normal
/// 这个点的结束时间存储在tangent.w中,这个也是刷新的根据时间
/// color存储了这批点的射中类型,x为1时就是第一条射线射中,y为1就是第二条射线射中
/// 为了物理模拟,移动时间设在uv6的x中
/// </summary>
private void AddMesh()
{
    //表示没有开始循环
    circulatePos = 0;
    particleSize -= particleSize % 3;

    poss = new Vector3;
    tris = new int;
    tangents = new Vector4;
    normals = new Vector3;
    uv0 = new Vector2;    //Texcoord0
    uv1 = new Vector2;    //Texcoord1
    uv2 = new Vector2;    //Texcoord2
    uv3 = new Vector2;    //Texcoord3
    uv4 = new Vector2;    //Texcoord4
    uv5 = new Vector2;    //Texcoord5
    uv6 = new Vector2;    //Texcoord6
    colors = new Color;

    //三个三个的加
    for (int i = 0; i < particleSize; i += 3)
    {
      poss = new Vector3(0, 0, 0);
      poss = new Vector3(0, 0, 1);
      poss = new Vector3(1, 0, 0);
      tris = i;
      tris = i + 1;
      tris = i + 2;
      //设置结束时间为负数,让Shader知道这个属性没有在使用中,
      //因为只有当前时间在终止时间和终止时间减存活时间之间才会开始运行
      tangents = new Vector4(0, 0, 0, -100);
      tangents = new Vector4(0, 0, 0, -100);
      tangents = new Vector4(0, 0, 0, -100);
      normals = Vector3.zero;
      normals = Vector3.zero;
      normals = Vector3.zero;

      uv0 = Vector2.zero;
      uv0 = Vector2.zero;
      uv0 = Vector2.zero;

      uv1 = Vector2.zero;
      uv1 = Vector2.zero;
      uv1 = Vector2.zero;

      uv2 = Vector2.zero;
      uv2 = Vector2.zero;
      uv2 = Vector2.zero;

      uv3 = Vector2.zero;
      uv3 = Vector2.zero;
      uv3 = Vector2.zero;

      uv4 = Vector2.zero;
      uv4 = Vector2.zero;
      uv4 = Vector2.zero;

      uv5 = Vector2.zero;
      uv5 = Vector2.zero;
      uv5 = Vector2.zero;

      uv6 = Vector2.zero;
      uv6 = Vector2.zero;
      uv6 = Vector2.zero;

      colors = 403 Forbidden;
      colors = 403 Forbidden;
      colors = 403 Forbidden;
    }

    mesh = new Mesh();
    mesh.vertices = poss;
    mesh.triangles = tris;
    mesh.tangents = tangents;
    mesh.uv = uv0;
    mesh.uv2 = uv1;
    mesh.uv3 = uv2;
    mesh.uv4 = uv3;
    mesh.uv5 = uv4;
    mesh.uv6 = uv5;
    mesh.uv7 = uv6;
    mesh.normals = normals;
    mesh.colors = colors;
    meshFilter.mesh = mesh;
}


5.传递数据

首先是第一个射线射中的数据传递方式
/// <summary>
/// 第一条射线射中目标
/// </summary>
/// <param name="raycastHit">射中点数据</param>
/// <param name="dis">理论上的最大距离,也就是本来这条线的长度</param>
/// <param name="sqrTrue">中间射中了东西,所以确定实际长度</param>
private void OneRayHit(RaycastHit raycastHit, float dis, float moveTime)
{
    SetOneRayPoint(raycastHit, dis, moveTime, circulatePos);
    SetOneRayPoint(raycastHit, dis, moveTime, circulatePos + 1);
    SetOneRayPoint(raycastHit, dis, moveTime, circulatePos + 2);
    meshFilter.sharedMesh.SetTangents(tangents);
    meshFilter.sharedMesh.SetColors(colors);
    meshFilter.sharedMesh.SetUVs(0, uv0);
    meshFilter.sharedMesh.SetUVs(1, uv1);
    meshFilter.sharedMesh.SetUVs(2, uv2);
    meshFilter.sharedMesh.SetUVs(3, uv3);
    meshFilter.sharedMesh.SetUVs(4, uv4);
    meshFilter.sharedMesh.SetUVs(6, uv6);
    meshFilter.sharedMesh.SetNormals(normals);
    circulatePos += 3;
}

private void SetOneRayPoint(RaycastHit raycastHit, float dis, float moveTime, int index)
{
   //表示这个粒子正在使用中
   tangents.w = Time.time + moveTime + outTime + offsetTime;

   //第一条射线射中
   colors = new Color(1, 0, 0, 1);

   //设置起始位置
   SetPos(ref uv0, transform.position);
   uv1.x = transform.position.z;

   //设置第一条贝塞尔曲线中点
   Vector3 dir = transform.forward * dis * 0.5f + transform.position;
   SetPos(ref uv2, dir);
   uv1.y = dir.z;


   //第一条线的终点
   SetPos(ref uv3, raycastHit.point);
   uv4.x = raycastHit.point.z;

   //射中的法线
   normals = raycastHit.normal;

   //设置粒子移动时间
   uv6.x = moveTime;
}
第二个点射中,或者压根就没射中
/// <summary>
/// 第二条射线射中的情况
/// </summary>
/// <param name="raycastHit">射线射中点信息</param>
/// <param name="upPos">第一条射线的终点</param>
/// <param name="firstSqrMax">第一条射线长度的平方</param>
/// <param name="fowardDir">第二条射线开始的方向,设为参数是为了考虑看向下方的情况</param>
private void TwoRayHit(RaycastHit raycastHit, Vector3 upPos, float dis, Vector3 fowardDir, float moveTime)
{
   SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos);
   SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos + 1);
   SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos + 2);
   circulatePos += 3;
   mesh.SetColors(colors);
   mesh.SetUVs(0, uv0);
   mesh.SetUVs(1, uv1);
   mesh.SetUVs(2, uv2);
   mesh.SetUVs(3, uv3);
   mesh.SetUVs(4, uv4);
   mesh.SetUVs(5, uv5);
   mesh.SetUVs(6, uv6);
   mesh.SetTangents(tangents);
   mesh.SetNormals(normals);
}

private void SetTwoRayPoint(RaycastHit raycastHit, Vector3 upPos,
   float dis, Vector3 fowardDir, float moveTime, int index)
{
   //表示这个粒子正在使用中
   tangents.w = Time.time + moveTime + outTime + offsetTime;


   colors = new Color(0f, 1f, 0f, 1f);

   //设置起始位置
   SetPos(ref uv0, transform.position);
   uv1.x = transform.position.z;

   //设置第一条射线中间位置
   Vector3 dir = transform.forward * dis * 0.5f + transform.position;
   SetPos(ref uv2, dir);
   uv1.y = dir.z;

   //设置第一条线的终点
   SetPos(ref uv3, upPos);
   uv4.x = upPos.z;
   //设置第二条线的中间位置,注意,为了方便,这个点的赋值方式有点特别
   fowardDir *= (raycastHit.point - upPos).magnitude * 0.5f;
   fowardDir += upPos;
   SetPos(ref uv5, fowardDir);
   uv4.y = fowardDir.z;

   //设置第二条线终点
   SetPos(ref tangents, raycastHit.point);

   //设置射中点的法线
   normals = raycastHit.normal;

   //设置粒子移动时间
   uv6.x = moveTime;

}
需要注意的是为了保证水能够正常射出,如果两条射线都没有射到东西时,将计算出来的第二个落点作为终点,也就是会在空中终结,这么实现肯定不是最好的,不过我只是提供一个思路,不代表是好的方案。
在代码数据传递时,我是一直计算一个面,也就是三个点三个点的赋值,在每一帧的开头,会循环一次找到可以赋值的三角面,如果没有直接退出,不进行排序是因为没有必要,搜索一遍的最大复杂度不超过o(n),同时因为是三个一次的遍历,每次赋值都是根据时间赋值的,如果一直射中一个点,因为赋的值一般是大于前面的,会渐渐变得有序。同时如果设置的初始化顶点够多,根本就不可能达到最大复杂度,如果设置的少,那一遍也没什么损耗,所以就直接遍历了。

6. 总代码

using UnityEngine;

namespace Common.ParticleSystem
{
    /// <summary>
    /// 水模拟粒子系统控制器,需要时时刷新数据,生成顶点
    /// </summary>
    public class ParticleWater : MonoBehaviour
    {

      private MeshFilter meshFilter;
      public Material setMat;

      /// <summary>      /// 循环到的位置      /// </summary>
      public int circulatePos;

      /// <summary>      /// 粒子输出花费时间      /// </summary>
      public float outTime = 0.3f;
      /// <summary>      /// 粒子到达后开始偏移的损耗时间      /// </summary>
      public float offsetTime = 2f;
      /// <summary>      /// 粒子数量,用来一开始创建      /// </summary>
      public int particleSize = 300;

      public float rayDis = 10;

      public LayerMask layer;

      #region CurveDate
      //移动大小曲线
      public bool isOpenMoveSizeCurve = false;
      public AnimationCurve moveSizeCurve = AnimationCurve.Linear(0,0,1,1);



      //偏移大小曲线
      public bool isOpenOffsetSizeCurve = false;
      public AnimationCurve offsetSizeCurve = AnimationCurve.Linear(0, 0, 1, 1);

      //透明曲线,因为透明都在片原着色器使用,因此设置一个就够了
      public bool isOpenAlphaCurve = false;
      public AnimationCurve offsetAlphaCurve = AnimationCurve.Linear(0, 1, 1, 0);

      //移动透明曲线
      public AnimationCurve moveAlphaCurve = AnimationCurve.Linear(0, 0, 1, 1);

      #endregion

      #region MeshDate            
      //Mesh数据设置位置,因为MeshFilter中的mesh设置没有效果,只能将Mesh
      //提出来然后每次设置为后赋值了

      private Mesh mesh;

      private Vector3[] poss;
      private int[] tris;
      private Vector4[] tangents;
      private Vector3[] normals;
      private Vector2[] uv0;
      private Vector2[] uv1;
      private Vector2[] uv2;
      private Vector2[] uv3;
      private Vector2[] uv4;
      private Vector2[] uv5;
      private Vector2[] uv6;
      private Color[] colors;

      #endregion

      private void Start()
      {
            meshFilter = GetComponent<MeshFilter>();
            if(meshFilter == null)
                meshFilter = gameObject.AddComponent<MeshFilter>();
            else
            {
                meshFilter.sharedMesh.Clear();
                meshFilter.sharedMesh = null;
            }
            AddMesh();
            SetMatValue();
      }

      private void OnValidate()
      {
            SetMatValue();
      }

      private void SetMatValue()
      {
            if (setMat == null) return;

            setMat.SetFloat("_OutTime", outTime);
            setMat.SetFloat("_OffsetTime", offsetTime);

            Vector4[] vector4;

            //设置移动大小
            vector4 = new Vector4;
            for (int i = 0; i < moveSizeCurve.length; i++)
            {
                vector4 = new Vector4(moveSizeCurve.keys.time, moveSizeCurve.keys.value,
                     moveSizeCurve.keys.inTangent, moveSizeCurve.keys.outTangent);
            }
            if (isOpenMoveSizeCurve) setMat.EnableKeyword("_MOVE_SIZE");
            else setMat.DisableKeyword("_MOVE_SIZE");
            setMat.SetInt("_MoveSizePointCount", moveSizeCurve.length);
            setMat.SetVectorArray("_MoveSizePointArray", vector4);

            //设置透明
            if (isOpenAlphaCurve) setMat.EnableKeyword("_ALPHA");
            else setMat.DisableKeyword("_ALPHA");

            //移动透明
            vector4 = new Vector4;
            for (int i = 0; i < moveAlphaCurve.length; i++)
            {
                vector4 = new Vector4(moveAlphaCurve.keys.time, moveAlphaCurve.keys.value,
                     moveAlphaCurve.keys.inTangent, moveAlphaCurve.keys.outTangent);
            }
            setMat.SetInt("_MoveAlphaPointCount", moveAlphaCurve.length);
            setMat.SetVectorArray("_MoveAlphaPointArray", vector4);


            //偏移透明
            vector4 = new Vector4;
            for (int i = 0; i < offsetAlphaCurve.length; i++)
            {
                vector4 = new Vector4(offsetAlphaCurve.keys.time, offsetAlphaCurve.keys.value,
                     offsetAlphaCurve.keys.inTangent, offsetAlphaCurve.keys.outTangent);
            }
            setMat.SetInt("_OffsetAlphaPointCount", offsetAlphaCurve.length);
            setMat.SetVectorArray("_OffsetAlphaPointArray", vector4);


            //设置大小
            vector4 = new Vector4;
            for (int i = 0; i < offsetSizeCurve.length; i++)
            {
                vector4 = new Vector4(offsetSizeCurve.keys.time, offsetSizeCurve.keys.value,
                  offsetSizeCurve.keys.inTangent, offsetSizeCurve.keys.outTangent);
            }


            if (isOpenOffsetSizeCurve)
                setMat.EnableKeyword("_OFFSET_SIZE");
            else setMat.DisableKeyword("_OFFSET_SIZE");
            setMat.SetInt("_OffsetSizePointCount", offsetSizeCurve.length);
            setMat.SetVectorArray("_OffsetSizePointArray", vector4);

            //设置位置
            setMat.SetVector("_BeginPos", transform.position);
      }

      /// <summary>
      /// 用来存储是否射线
      /// </summary>
      bool desireRay;

      public void RayWater()
      {
            desireRay = true;
      }

      private void FixedUpdate()
      {
            //if (!desireRay) return;
            //else desireRay = false;
            circulatePos %= particleSize;
            //检查粒子是否可以使用,因为所有粒子是顺序执行的,不能用就直接退出
            int i = 0;
            for (; i<particleSize; i+=3)
            {
                //可以使用,进行操作
                if (meshFilter.sharedMesh.tangents[(circulatePos + i)%particleSize].w < Time.time)
                {
                  circulatePos = circulatePos + i;
                  break;
                }
            }
            if (i >= particleSize) return;


            RaycastHit raycastHit;

            Vector3 upDir = transform.forward * rayDis;
            Vector3 upPos = transform.position;
            Vector3 veTemp = Vector3.zero;
            float upTime = 0;


            //向上时执行上抛,否则直接向下确定位置
            if (transform.forward.y >= 0)
            {
                upTime = upDir.y / 9.8f;

                //确定第一条射线数据
                upDir.y = 0;
                upPos.y += 0.5f * 9.8f * upTime * upTime;
                upPos += upDir * upTime;
                veTemp = upPos - transform.position;
                //第一条线射中目标,检查是否上方有东西阻挡
                if (Physics.Raycast(transform.position, veTemp, out raycastHit, veTemp.magnitude, layer))
                {
                  OneRayHit(raycastHit, raycastHit.distance, upTime);
                  Debug.DrawLine(transform.position, raycastHit.point, 403 Forbidden);
                  return;
                }
            }

            //默认底部高度
            float buttonY = transform.position.y - 1;

            if (Physics.Raycast(transform.position, Vector3.down, out raycastHit, layer))
            {
                buttonY = raycastHit.point.y;
            }

            float s = upPos.y - buttonY;

            float downTime = Mathf.Sqrt(
                2 * (0.5f * 9.8f * Mathf.Pow(upDir.y / 9.8f, 2) + s) / 9.8f
                ) - Mathf.Abs(upDir.y) / 9.8f;
            upDir.y = 0;
            Vector3 downPos = downTime * upDir + transform.position;
            downPos.y = buttonY;


            //第二条射线默认无限距离,往尽头射
            if (Physics.Raycast(upPos, downPos - upPos, out raycastHit, layer))
            {
                TwoRayHit(raycastHit, upPos, veTemp.magnitude, transform.forward, downTime + upTime);
                Debug.DrawLine(upPos, raycastHit.point, 403 Forbidden);

                return;
            }

            //第二条也没有中,就在空中结束吧
            raycastHit.point = downPos;
            raycastHit.normal = Vector3.up;
            TwoRayHit(raycastHit, upPos, veTemp.magnitude, transform.forward, downTime + upTime);

            Debug.DrawLine(upPos, raycastHit.point, Color.white);
      }


      /// <summary>
      /// 第一条射线射中目标
      /// </summary>
      /// <param name="raycastHit">射中点数据</param>
      /// <param name="dis">理论上的最大距离,也就是本来这条线的长度</param>
      /// <param name="sqrTrue">中间射中了东西,所以确定实际长度</param>
      private void OneRayHit(RaycastHit raycastHit, float dis, float moveTime)
      {
            SetOneRayPoint(raycastHit, dis, moveTime, circulatePos);
            SetOneRayPoint(raycastHit, dis, moveTime, circulatePos + 1);
            SetOneRayPoint(raycastHit, dis, moveTime, circulatePos + 2);
            meshFilter.sharedMesh.SetTangents(tangents);
            meshFilter.sharedMesh.SetColors(colors);
            meshFilter.sharedMesh.SetUVs(0, uv0);
            meshFilter.sharedMesh.SetUVs(1, uv1);
            meshFilter.sharedMesh.SetUVs(2, uv2);
            meshFilter.sharedMesh.SetUVs(3, uv3);
            meshFilter.sharedMesh.SetUVs(4, uv4);
            meshFilter.sharedMesh.SetUVs(6, uv6);
            meshFilter.sharedMesh.SetNormals(normals);
            circulatePos += 3;
      }

      private void SetOneRayPoint(RaycastHit raycastHit, float dis, float moveTime, int index)
      {
            //表示这个粒子正在使用中
            tangents.w = Time.time + moveTime + outTime + offsetTime;

            //第一条射线射中
            colors = new Color(1, 0, 0, 1);

            //设置起始位置
            SetPos(ref uv0, transform.position);
            uv1.x = transform.position.z;

            //设置第一条贝塞尔曲线中点
            Vector3 dir = transform.forward * dis * 0.5f + transform.position;
            SetPos(ref uv2, dir);
            uv1.y = dir.z;


            //第一条线的终点
            SetPos(ref uv3, raycastHit.point);
            uv4.x = raycastHit.point.z;

            //射中的法线
            normals = raycastHit.normal;

            //设置粒子移动时间
            uv6.x = moveTime;
      }

      /// <summary>
      /// 第二条射线射中的情况
      /// </summary>
      /// <param name="raycastHit">射线射中点信息</param>
      /// <param name="upPos">第一条射线的终点</param>
      /// <param name="firstSqrMax">第一条射线长度的平方</param>
      /// <param name="fowardDir">第二条射线开始的方向,设为参数是为了考虑看向下方的情况</param>
      private void TwoRayHit(RaycastHit raycastHit, Vector3 upPos, float dis, Vector3 fowardDir, float moveTime)
      {
            SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos);
            SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos + 1);
            SetTwoRayPoint(raycastHit, upPos, dis, fowardDir, moveTime, circulatePos + 2);
            circulatePos += 3;
            mesh.SetColors(colors);
            mesh.SetUVs(0, uv0);
            mesh.SetUVs(1, uv1);
            mesh.SetUVs(2, uv2);
            mesh.SetUVs(3, uv3);
            mesh.SetUVs(4, uv4);
            mesh.SetUVs(5, uv5);
            mesh.SetUVs(6, uv6);
            mesh.SetTangents(tangents);
            mesh.SetNormals(normals);
      }

      private void SetTwoRayPoint(RaycastHit raycastHit, Vector3 upPos,
            float dis, Vector3 fowardDir, float moveTime, int index)
      {
            //表示这个粒子正在使用中
            tangents.w = Time.time + moveTime + outTime + offsetTime;


            colors = new Color(0f, 1f, 0f, 1f);

            //设置起始位置
            SetPos(ref uv0, transform.position);
            uv1.x = transform.position.z;

            //设置第一条射线中间位置
            Vector3 dir = transform.forward * dis * 0.5f + transform.position;
            SetPos(ref uv2, dir);
            uv1.y = dir.z;

            //设置第一条线的终点
            SetPos(ref uv3, upPos);
            uv4.x = upPos.z;
            //设置第二条线的中间位置,注意,为了方便,这个点的赋值方式有点特别
            fowardDir *= (raycastHit.point - upPos).magnitude * 0.5f;
            fowardDir += upPos;
            SetPos(ref uv5, fowardDir);
            uv4.y = fowardDir.z;

            //设置第二条线终点
            SetPos(ref tangents, raycastHit.point);

            //设置射中点的法线
            normals = raycastHit.normal;

            //设置粒子移动时间
            uv6.x = moveTime;

      }

      /// <summary>      /// 将第二个参数的xyz值赋予第一个参数的xyz中,简化上面的函数      /// </summary>
      private void SetPos(ref Vector4 vector, Vector3 vector3)
      {
            vector.x = vector3.x;
            vector.y = vector3.y;
            vector.z = vector3.z;
      }

      /// <summary>      /// 将第二个参数的xyz值赋予第一个参数的xyz中,简化上面的函数      /// </summary>
      private void SetPos(ref Vector2 vector, Vector3 vector3)
      {
            vector.x = vector3.x;
            vector.y = vector3.y;
      }


      /// <summary>
      /// 用来初始化这个生成的顶点,为了画出较好的贝塞尔曲线,
      /// 且保证每一边都是贝塞尔曲线,打算将三个点的数据传入其中
      /// 首先顶点数据依旧用来存储随机数,这个数据是固定不变的,
      /// 第一条射线的起点存储在uv0和uv1(x)中,即begin=(uv0.xy, uv1.x)
      /// 第一条射线的贝塞尔曲线 中点 存储在uv1(y)和uv2中,其中center=(uv2.xy, uv1.y)
      /// 第一条射线的终点[也是第二条射线的起点]在uv3和uv4(x)中,即end=(uv3.xy, uv4.x)
      /// 第二条射线的贝塞尔曲线 中点 存储在uv4(y)和uv5中,其中center=(uv5.xy, uv4.y)
      /// 第二条射线的终点存储在tangent中,其中end=(Corporate governance and insider trading resources)
      /// 射线的最终点的法线存储在normal中,就是normal = normal
      /// 这个点的结束时间存储在tangent.w中,这个也是刷新的根据时间
      /// color存储了这批点的射中类型,x为1时就是第一条射线射中,y为1就是第二条射线射中
      /// 为了物理模拟,移动时间设在uv6的x中
      /// </summary>
      private void AddMesh()
      {
            //表示没有开始循环
            circulatePos = 0;
            particleSize -= particleSize % 3;

            poss = new Vector3;
            tris = new int;
            tangents = new Vector4;
            normals = new Vector3;
            uv0 = new Vector2;    //Texcoord0
            uv1 = new Vector2;    //Texcoord1
            uv2 = new Vector2;    //Texcoord2
            uv3 = new Vector2;    //Texcoord3
            uv4 = new Vector2;    //Texcoord4
            uv5 = new Vector2;    //Texcoord5
            uv6 = new Vector2;    //Texcoord6
            colors = new Color;

            //三个三个的加
            for (int i = 0; i < particleSize; i += 3)
            {
                poss = new Vector3(0, 0, 0);
                poss = new Vector3(0, 0, 1);
                poss = new Vector3(1, 0, 0);
                tris = i;
                tris = i + 1;
                tris = i + 2;
                //设置结束时间为负数,让Shader知道这个属性没有在使用中,
                //因为只有当前时间在终止时间和终止时间减存活时间之间才会开始运行
                tangents = new Vector4(0, 0, 0, -100);
                tangents = new Vector4(0, 0, 0, -100);
                tangents = new Vector4(0, 0, 0, -100);
                normals = Vector3.zero;
                normals = Vector3.zero;
                normals = Vector3.zero;

                uv0 = Vector2.zero;
                uv0 = Vector2.zero;
                uv0 = Vector2.zero;

                uv1 = Vector2.zero;
                uv1 = Vector2.zero;
                uv1 = Vector2.zero;

                uv2 = Vector2.zero;
                uv2 = Vector2.zero;
                uv2 = Vector2.zero;

                uv3 = Vector2.zero;
                uv3 = Vector2.zero;
                uv3 = Vector2.zero;

                uv4 = Vector2.zero;
                uv4 = Vector2.zero;
                uv4 = Vector2.zero;

                uv5 = Vector2.zero;
                uv5 = Vector2.zero;
                uv5 = Vector2.zero;

                uv6 = Vector2.zero;
                uv6 = Vector2.zero;
                uv6 = Vector2.zero;

                colors = 403 Forbidden;
                colors = 403 Forbidden;
                colors = 403 Forbidden;
            }

            mesh = new Mesh();
            mesh.vertices = poss;
            mesh.triangles = tris;
            mesh.tangents = tangents;
            mesh.uv = uv0;
            mesh.uv2 = uv1;
            mesh.uv3 = uv2;
            mesh.uv4 = uv3;
            mesh.uv5 = uv4;
            mesh.uv6 = uv5;
            mesh.uv7 = uv6;
            mesh.normals = normals;
            mesh.colors = colors;
            meshFilter.mesh = mesh;
      }
    }
}
传递完数据后就到Shader的模拟阶段了,这个阶段放到下一篇文章,毕竟一次写太多不太好,容易讲的不清楚。

总结

以上就是这篇文章的全部内容,这篇文章只是起始部分,在之后的着色部分才是大头,毕竟要一整套流程搞下来还是内容挺多的,这里只是最简单的数据传递部分而已。
实际上这里的数据传递还有可以优化的部分,因为直接使用Set来传递顶点数据的话是替换,从效率来看的话是很慢的,毕竟我们实际上只是改变部分值,却替换了整一个数组对象,因为C#和unity底层有区别,本质上模型存储的数据应该不是代码中控制的数组数据,因此在替换时会进行数据检测、替换等阶段,效率很低。
在看unity官方文档时看到流操作传递数据,既然是流,那一定很快啊,但是官方文档的操作写的好随便,网上也没有找到资料使用的教程,如果有人知道请在下面评论区解答一下,毕竟能优化是最好的。
页: [1]
查看完整版本: Unity实现基于粒子的水模拟(一:物理模拟)