深入解读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)!
深入解读Job system(2)的更多相关文章
- 深入解读Job System(1)
https://mp.weixin.qq.com/s/IY_zmySNrit5H8i0CcTR7Q 通常而言,最好不要把Unity实体组件系统ECS和Job System看作互相独立的部分,要把它们看 ...
- System.nanoTime与System.currentTimeMillis的理解与区别
System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包. 平时产生随机数时我们经常拿时间做种子,比如用System.currentTimeMillis的结果 ...
- System.currentTimeMillis()计算方式与时间的单位转换
目录[-] 一.时间的单位转换 二.System.currentTimeMillis()计算方式 一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 ...
- 关于System.currentTimeMillis()
一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 微秒(μs) 1微秒=1/1,000,000秒(s)1秒=1,000,000,000 纳秒(ns ...
- Java中系统时间的获取_currentTimeMillis()函数应用解读
快速解读 System.currentTimeMillis()+time*1000) 的含义 一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 微 ...
- I.MX6 Android 5.1 快速合成系统
/**************************************************************************** * I.MX6 Android 5.1 快速 ...
- java多态/重载方法——一个疑难代码引发的讨论
直接上代码,看这个代码发现自己的基础有多差了.参考 http://www.cnblogs.com/lyp3314/archive/2013/01/26/2877205.html和http://hxra ...
- 高通 添加 cmdline
最近需要设置一个只读的属性值,采用的方法是在cmdline中添加,然后在init进程中解读. 记录一下代码跟踪过程. lk/app/aboot/aboot.c static const char *u ...
- 韩顺平Java(持续更新中)
原创上课笔记,转载请注明出处 第一章 面向对象编程(中级部分) PDF为主 1.1 IDEA 删除当前行,ctrl+y 复制当前行,ctrl+d 补全代码,alt+/ 添加或者取消注释,ctrl+/ ...
随机推荐
- linux php相关命令
学习源头:http://www.cnblogs.com/myjavawork/articles/1869205.html php -m 查看php开启的相关模块 php -v 查看php的版本 运行直 ...
- Windows命令查看文件的MD5/SHA1/SHA256
certutil -hashfile "D:\Tools\Microsoft\SqlServer\2016\ct_sql_server_2016_enterprise_x64_dvd_869 ...
- UEditor富文本编辑器的使用 http://fex.baidu.com/ueditor/
[转] http://fex.baidu.com/ueditor/ UEditor 介绍 UEditor 是由百度「FEX前端研发团队」开发的所见即所得富文本web编辑器,具有轻量,可定制,注重用户体 ...
- My97DatePicker 和转换 数据库中日期(/Date(1351699200000)/) 的格式
一 转换 数据库中日期(/Date(1351699200000)/) 的格式: C#中转换日期格式 var date=com.CREATEDATETIME.ToString(); JavaScrip ...
- [转] pip镜像升级报警 -trust-host问题解决方案
pip升级到7.0以后,在使用http镜像进行包安装及升级的时候往往会有如下提示: Collecting beautifulsoup4The repository located at mirrors ...
- Spring线程池由浅入深的3个示例
作者博客主页:http://blog.csdn.net/chszs 本文提供了三个Spring多线程开发的例子,由浅入深,由于例子一目了然,所以并未做过多的解释.诸位一看便知. 前提条件: 1)在Ec ...
- AngularJS:实例
ylbtech-AngularJS:实例 1.返回顶部 1. AngularJS 实例 实例 您可以在线编辑实例,然后点击按钮查看结果. AngularJS 实例 <div ng-app=&qu ...
- python风格指南
还在让别人一眼就看出你是一只野生程序猿嘛,快来看看谷歌的python风格指南提升逼格吧! http://zh-google-styleguide.readthedocs.io/en/latest/go ...
- 2016.4.6 WinForm显示PDF两种方法
1.最直接的方法,添加webbrowser控件 webb.Url = new Uri(path);可显示pdf控件. 如果需要在打开时跳转到某页,可用在路径后直接加#page=,例如webb.Url ...
- 向linux内核增加一个系统调用-1
验证编辑编译内核的流程,并增加新的系统调用 注意:需要/目录至少10GB空间,/boot目录500MB空间 下载内核并解压 kernel下载 百度云搬运 密码: qc8b 进入 /usr/src目录 ...