1. 程式人生 > >深入解讀Job System(2)

深入解讀Job System(2)

該專案中,我們將程式化生成一個平面,然後使用滑鼠點選輸入來生成球體,然後球體會在平面上產生凹槽,該功能可以用於實現腳印的效果。此專案只是使用Unity的Job System來實現高效網格變形的一個開端。

訪問程式碼

本文程式碼你可以在GitHub上檢視:

首先編寫生成平面的程式碼。建立一個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,該程式碼是執行作業的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變數中迴圈,然後比較半徑大小,如果符合條件,我們會給頂點新增負作用力值,從而造成下圖中的凹槽。順便一提,如果作用力為負,頂點會上升而不是下沉。