• 深入解读Job system(2)


    https://mp.weixin.qq.com/s/vV4kqorvMtddjrrjmOxQKg

    上一篇文章中,我们讲解了Job System的基础知识,本文将以网格变形项目为示例,讲解Job System的使用。

    该项目中,我们将程序化生成一个平面,然后使用鼠标点击输入来生成球体,然后球体会在平面上产生凹槽,该功能可以用于实现脚印的效果。此项目只是使用Unity的Job System来实现高效网格变形的一个开端。

    访问代码

    本文代码你可以在GitHub上查看:

    https://github.com/itsKristin/Jobified-Meshdeformation

    DeformableMesh.cs

    首先编写生成平面的代码。创建一个C#脚本,命名为DeformableMesh。我们将加入using Unity.Collections声明,因为我们需要使用NativeArrays和Unity.Jobs,而且作业要继承自IJobParalelFor。

    我们要定义几个变量,用来帮助定义程序化生成平面的大小、作用力和半径,在变形部分时会用到这些变量,我们还在Awake函数缓存了所有需要用于渲染网格的信息。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Unity.Collections;
    using Unity.Jobs;

    [RequireComponent(typeof(MeshFilter),(typeof(MeshRenderer)))]
    public class DeformableMesh : MonoBehaviour 
    {
     [Header("Size Settings:")]
     [SerializeField] float verticalSize;
     [SerializeField] float horizontalSize;

     [Header("Material:")]
     [SerializeField] Material meshMaterial;

     [Header("Indentation Settings:")]
     [SerializeField] float force;
     [SerializeField] float radius;

     Mesh mesh;
     MeshFilter meshFilter;
     MeshRenderer meshRenderer;
     MeshCollider meshCollider;

     //网格信息

     Vector3[] vertices;
     Vector3[] modifiedVertices;
     int[] triangles;

     Vector2 verticeAmount;

     void Awake() 
     {
       meshRenderer = GetComponent<MeshRenderer>();
       meshFilter = GetComponent<MeshFilter>();
       meshFilter.mesh = new Mesh();
       mesh = meshFilter.mesh;
       GeneratePlane();
     }

    仔细观察代码以及注释内容,了解如何通过代码程序化生成平面。

    /*网格是由顶点和三角形构建的,基本上由其中的三个顶点构建。我们首先处理顶点的位置。

    顶点需要Vector3数组,因为它们在世界空间中拥有3D位置。数组的长度取决于所生成平面的大小。

    简单来说,可以想象平面顶部有网格覆盖,每个网格区域的每个角都需要一个顶点,相邻区域可以共享同一个角。因此,在每个维度中,顶点的数量需要比区域的数量多1。*/

    void GeneratePlane()
    {
     vertices = new Vector3[((int)horizontalSize + 1) * 
     ((int)verticalSize + 1)];
     Vector2[] uv = new Vector2[vertices.Length];

      /*现在使用嵌套的for循环相应地定位顶点*/

     for(int z = 0, y = 0; y <= (int)verticalSize; y++)
     {
       for(int x = 0; x <= (int)horizontalSize; x++, z++)
       {
         vertices[z] = new Vector3(x,0,y);
         uv[z] = new Vector2(x/(int)horizontalSize,
         y/(int)verticalSize);
       }
     }

      /*我们已经生成并定位了顶点,应该开始生成合适的网格。

      首先设置这些顶点为网格顶点*/

     mesh.vertices = vertices;

      /*我们还需要确保我们的顶点和修改的顶点在一开始就相互匹配*/

     modifiedVertices = new Vector3[vertices.Length];
     for(int i = 0; i < vertices.Length; i++)
     {
       modifiedVertices[i] = vertices[i];
     }
     mesh.uv = uv;

      /*网格此时还不会出现,因为它没有任何三角形。我们会通过循环构成三角形的点来生成三角形,这些三角形的标签会进入int类型的triangles数组中*/

     triangles = new int[(int)horizontalSize * 
     (int)verticalSize * 6];

     for(int t = 0, v = 0, y = 0; y < (int)verticalSize; y++, v++)
     {
       for(int x = 0; x <(int)horizontalSize; x++, t+= 6, v++)
       {
         triangles[t] = v;
         triangles[t + 3] = triangles[t + 2] = v + 1; 
         triangles[t + 4] = triangles[t + 1] = v + (int)horizontalSize + 1;
         triangles[t + 5] = v + (int)horizontalSize + 2;
       }
     }

      /*最后,我们需要将三角形指定为网格三角形,然后重新计算法线,确保得到正确的光照效果*/

     mesh.triangles = triangles;
     mesh.RecalculateNormals();
     mesh.RecalculateBounds();
     mesh.RecalculateTangents();

      /*我们还需要碰撞体,从而能够使用物理系统检测交互*/

     meshCollider = gameObject.AddComponent<MeshCollider>();
     meshCollider.sharedMesh = mesh;

      //我们需要设置网格材质,以避免出现难看的红色平面

     meshRenderer.material = meshMaterial;
    }

    我们使用了不同的方法进行碰撞检测,MouseInput脚本会触发一个协程,该协程会在平面上创建圆形球体并留下凹槽。

    void OnCollisionEnter(Collision other) {
       if(other.contacts.Length > 0)
       {
        Vector3[] contactPoints = new Vector3[other.contacts.Length];
         for(int i = 0; i < other.contacts.Length; i++)
         {
           Vector3 currentContactpoint = other.contacts[i].point;
           currentContactpoint = transform.InverseTransformPoint(currentContactpoint);
           contactPoints[i] = currentContactpoint;
         }
         IndentSnow(force,contactPoints);
       }
     }

     public void AddForce(Vector3 inputPoint)
     {
       StartCoroutine(MarkHitpointDebug(inputPoint));
     }

     IEnumerator MarkHitpointDebug(Vector3 point)
     {
       GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Sphere);
       marker.AddComponent<SphereCollider>();
       marker.AddComponent<Rigidbody>();
       marker.transform.position = point;
       yield return new WaitForSeconds(0.5f);
       Destroy(marker);
     }

    现在来到了重点部分,调度作业。我们将在这部分可视化说明了解调度作业的方法的重要性。

    第一个代码段是个调度作业的方法,可以复制该代码段到自己的项目中,然而它执行的效果不如预期的高效。原因很简单,我们可能会使用到IJobParalelFor,但并没有让作业并行执行,因为我们会在调度后马上调用Complete, 这样就会导致执行还是需要一个一个的来。

    public void IndentSnow(float force, Vector3[] worldPositions)
     {
       NativeArray<Vector3> contactpoints = new NativeArray<Vector3>
       (worldPositions, Allocator.TempJob);
       NativeArray<Vector3> initialVerts = new NativeArray<Vector3>
     (vertices, Allocator.TempJob);
     NativeArray<Vector3> modifiedVerts = new NativeArray<Vector3>
    (modifiedVertices, Allocator.TempJob);
     
     IndentationJob meshIndentationJob = new IndentationJob
    {
          contactPoints = contactpoints,
          initialVertices = initialVerts,
          modifiedVertices = modifiedVerts,
          force = force,
          radius = radius
     };

     JobHandle indentationJobhandle = meshIndentationJob.Schedule(initialVerts.Length,initialVerts.Length);
     indentationJobhandle.Complete();
     
       contactpoints.Dispose();
       initialVerts.Dispose();
       modifiedVerts.CopyTo(modifiedVertices);
       modifiedVerts.Dispose();

       mesh.vertices = modifiedVertices;
       vertices = mesh.vertices;
       mesh.RecalculateNormals();
     }

    现在查看下图性能分析器。

    仔细注意到上图中的工作线程,你会看到所有线程中的等待时间,这是因为我们没有相应地调度作业。希望上图能清楚告诉你调度的重要性。

    下面我们来进行正确的调度作业。

    后面的代码段中,我们会创建一个类,它将帮助我保存本地数组和作业句柄。我会跟踪已创建的每个作业,然后在Update中从循环代码完成它。

    在调度要执行的作业前,我们定义了一些变量,下面的代码段中我们没有使用Vector3的常规数组,而是使用了NativeArray<Vector3>。NativeArrays中添加了Job System命名空间,从而确保能够安全地处理多线程代码。

    如前文所说,这些数组和常规数组不同,因为你必须定义一个分配器。这基本上是NativeArrays持续性和分配过程的数值。这些数组还不会受到垃圾收集过程的影响,因此它们和本地代码相似,所以你需要手动除去或释放这些数组。

    void IndentSnow(float force, Vector3[] worldPositions,ref HandledResult newHandledResult)
     {

       newHandledResult.contactpoints = new NativeArray<Vector3>
       (worldPositions, Allocator.TempJob);
       newHandledResult.initialVerts = new NativeArray<Vector3>
     (vertices, Allocator.TempJob);
       newHandledResult.modifiedVerts = new NativeArray<Vector3>
    (modifiedVertices, Allocator.TempJob);
     
     IndentationJob meshIndentationJob = new IndentationJob
    {
          contactPoints = newHandledResult.contactpoints,
          initialVertices = newHandledResult.initialVerts,
          modifiedVertices = newHandledResult.modifiedVerts,
          force = force,
          radius = radius
     };

     JobHandle indentationJobhandle = meshIndentationJob.Schedule(newHandledResult.initialVerts.Length,newHandledResult.initialVerts.Length);
     
       newHandledResult.jobHandle = indentationJobhandle;

       scheduledJobs.Add(newHandledResult);
     }

     void CompleteJob(HandledResult handle)
     {
       scheduledJobs.Remove(handle);

       handle.jobHandle.Complete();
     
       handle.contactpoints.Dispose();
       handle.initialVerts.Dispose();
       handle.modifiedVerts.CopyTo(modifiedVertices);
       handle.modifiedVerts.Dispose();

       mesh.vertices = modifiedVertices;
       vertices = mesh.vertices;
       mesh.RecalculateNormals();
         
     }
    }

    struct HandledResult
    {
     public JobHandle jobHandle;
     public NativeArray<Vector3> contactpoints;
     public NativeArray<Vector3> initialVerts;
     public NativeArray<Vector3> modifiedVerts;
    }

    最后,性能分析器会告诉新代码的效率明显高了很多。

    IndentationJob.cs

    最后需要编写IndentationJob.cs,该代码是执行作业的struct。作为作业,它也继承自IJob接口,本示例中是IJobParallelFor,它最后会对网格变形产生影响,因为想要让它在每个作业多次运行,我们将调用作业的执行函数,调用次数等于网格顶点的数量。

    你编写的每个作业都必须拥有Execute()函数,因为你需要通过该函数添加自定义代码到作业中。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Unity.Collections;
    using Unity.Jobs;

    public struct IndentationJob : IJobParallelFor {

     public NativeArray<Vector3> contactPoints;
     public NativeArray<Vector3> initialVertices;
     public NativeArray<Vector3> modifiedVertices;

     public float force;
     public float radius;

     public void Execute(int i)
     {
       for(int c = 0; c < contactPoints.Length; c++)
       {
         Vector3 pointToVert = (modifiedVertices[i] - contactPoints[c]);
         float distance = pointToVert.sqrMagnitude;

         if(distance < radius)
         {
           Vector3 newVertice = initialVertices[i] + Vector3.down * (force);
           modifiedVertices[i] = newVertice;
         }
       }
     }
    }

    在Execute()函数中,我们在顶点和特定在碰撞球体时缓存contactPoints变量中循环,然后比较半径大小,如果符合条件,我们会给顶点添加负作用力值,从而造成下图中的凹槽。顺便一提,如果作用力为负,顶点会上升而不是下沉。

    小结

    本文将以网格变形项目为示例,讲解Job System的使用就介绍到这里,希望大家学以致用,熟练掌握Job System。Unity更多内容介绍尽在Unity官方中文论坛(UnityChina.cn)!

  • 相关阅读:
    Java知识点总结
    Eclipse使用中遇到的问题及解决
    Ubuntu--64位系统安装32位的库
    Ubuntu--防火墙的使用
    Ubuntu--手动配置IP地址,网关,DNS
    tcp echo_server
    模板类继承-成员变量不可访问的问题
    brpc channel -负载均衡-NamingService三者之间的关系
    头文件依赖顺序风格
    brpc namingservice的调用栈
  • 原文地址:https://www.cnblogs.com/nafio/p/9794286.html
Copyright © 2020-2023  润新知