在Unity UI中创建可视组件。 粒子系统

你好 本文旨在使用在Canvas'e中可视化粒子系统的组件示例,在UI中创建自己的可视组件。

该信息对于在用户界面中实现各种效果非常有用,也可以用于生成网格或对其进行优化。

图片


一些理论或从何处开始创建组件


Unity UI的基础是Canvas 。 根据UI元素的内部层次结构,渲染系统使用它来显示“多层”几何图形。
任何可视用户界面组件都必须从Graphic类(或其派生的MaskableGraphic类)继承,该类将所有必需的数据传递到CanvasRenderer组件以进行渲染。 数据是在OnPopulateMesh方法中创建的,每次在组件需要更新其几何形状时(例如,在调整元素大小时)都会调用该方法。 VertexHelper作为参数传递,这有助于为UI生成网格。

组件创建


基数


我们通过创建一个从MaskableGraphic类继承的UIParticleSystem脚本开始实现。 MaskableGraphicGraphic类的派生类,此外还提供使用蒙版的工作。 重写OnPopulateMesh方法。 使用VertexHelper生成网格粒子系统网格的顶点的基础如下所示:

public class UIParticleSystem : MaskableGraphic { protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); ... int particlesCount = ... ; for (int i = 0; i < particlesCount; i++) { Color vertexColor = ... ; Vector2[] vertexUV = ... ; UIVertex[] quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 vertixPosition = ... ; quadVerts[j] = new UIVertex() { position = vertixPosition, color = vertexColor, uv0 = vertexUV }; } vh.AddUIVertexQuad(quadVerts); } } } 

首先,您需要通过调用Clear方法从现有数据中清除VertextHelper 。 之后,您可以开始使用有关峰的新数据填充它。 为此,将使用AddUIVertexQuad方法,该方法允许您一次添加有关4个顶点的信息。 选择此方法是为了易于使用,因为 每个粒子都是一个矩形。 每个顶点由一个UIVertex对象描述。 在所有参数中,我们只需要填写uv扫描的位置,颜色和一些坐标。

顶点助手
VertexHelper具有用于添加顶点信息的整套方法,以及用于接收当前数据的一对方法。 对于更复杂的几何图形,最好的解决方案是选择AddUIVertexStream方法,该方法接受一个顶点列表和一个索引列表。

由于每帧粒子的位置,其颜色和其他参数都会改变,因此用于渲染的网格也应该更新。
为此,每个帧都将调用SetVerticesDirty方法,该方法将在需要重新计数新数据时设置标志,这将导致对OnPopulateMesh方法的调用。 类似地,对于一种材料,如果其属性发生变化,则需要调用SetMaterialDirty方法。

 protected void Update() { SetVerticesDirty(); } 

覆盖mainTexture属性。 它指示哪个纹理将传递到CanvasRenderer并在材质中使用_MainTex shader 属性 。 为此,创建一个ParticleImage字段,该字段将由mainTexture属性返回。

 public Texture ParticleImage; public override Texture mainTexture { get { return ParticleImage; } } 

粒子系统


用于生成网格顶点的数据将从“ 粒子系统”组件中获取,该组件参与有关粒子位置,其大小,颜色等的所有计算。
粒子渲染涉及到需要禁用的ParticleSystemRenderer组件,因此其他组件UIParticleSystemCanvasRenderer将负责创建网格并将其渲染到UI中。

创建操作所需的字段,并在Awake方法中对其进行初始化。

UI行为
与大多数方法一样, Awake需要在此处重新定义,因为它们在UIBehaviour中被列为虚拟。 UIBehaviour本身是抽象的,实际上不包含任何工作逻辑,但对于Graphic类来说是基本的。

 private ParticleSystem _particleSystem; private ParticleSystemRenderer _particleSystemRenderer; private ParticleSystem.MainModule _main; private ParticleSystem.Particle[] _particles; protected override void Awake() { base.Awake(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; int maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; } 

_particles字段将用于存储ParticleSystem粒子,并且
_main用于MainModule模块以方便使用。

让我们添加OnPopulateMesh方法,直接从粒子系统获取所有必要的数据。 创建辅助变量Vector3 [] _quadCornersVector2 [] _simpleUV

_quadCorners包含矩形四个角相对于粒子中心的坐标。 每个粒子的初始大小被认为是边为1x1的正方形。
_simpleUV - uv扫描的坐标,在这种情况下,所有粒子都使用相同的纹理而没有任何位移。

 private Vector3[] _quadCorners = new Vector3[] { new Vector3(-.5f, -.5f, 0), new Vector3(-.5f, .5f, 0), new Vector3(.5f, .5f, 0), new Vector3(.5f, -.5f, 0) }; private Vector2[] _simpleUV = new Vector2[] { new Vector2(0,0), new Vector2(0,1), new Vector2(1,1), new Vector2(1,0), }; 

 protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) { var particle = _particles[i]; Vector3 particlePosition = particle.position; Color vertexColor = particle.GetCurrentColor(_particleSystem) * color; Vector3 particleSize = particle.GetCurrentSize3D(_particleSystem); Vector2[] vertexUV = _simpleUV; Quaternion rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); UIVertex[]quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 cornerPosition = rotation * Vector3.Scale(particleSize, _quadCorners[j]); Vector3 vertexPosition = cornerPosition + particlePosition; vertexPosition.z = 0; quadVerts[j] = new UIVertex(); quadVerts[j].color = vertexColor; quadVerts[j].uv0 = vertexUV[j]; quadVerts[j].position = vertexPosition; } vh.AddUIVertexQuad(quadVerts); } } 

顶点位置
首先,计算顶点相对于粒子中心的局部位置,并考虑其大小(操作Vector3.Scale(粒子大小,_quadCorners [j]) )和旋转(将四元数旋转乘以矢量)。 将粒子本身的位置添加到结果中之后

现在,让我们使用标准组件为测试创建一个简单的UI。

图片

UIParticleSystem添加 ParticleSystem组件

图片

运行场景并检查组件的结果。

图片

根据粒子在层次结构中的位置显示它们,并考​​虑使用的蒙版。 当您更改屏幕的分辨率及其比例以及更改Canvas的“ 渲染模式”属性时,粒子的行为与Canvas中的任何其他可视组件相似,并且仅在其中显示。

模拟空间


因为 我们将粒子系统放置在UI内, SimulationSpace参数存在问题。 在世界空间中进行模拟时,不会在应有的位置显示粒子。 因此,我们根据参数值添加了粒子位置的计算。

 protected override void OnPopulateMesh(VertexHelper vh) { ... Vector3 particlePosition; switch (_main.simulationSpace) { case ParticleSystemSimulationSpace.World: particlePosition = _rectTransform.InverseTransformPoint(particle.position); break; case ParticleSystemSimulationSpace.Local: particlePosition = particle.position; break; case ParticleSystemSimulationSpace.Custom: if (_main.customSimulationSpace != null) particlePosition = _rectTransform.InverseTransformPoint( _main.customSimulationSpace.TransformPoint(particle.position) ); else particlePosition = particle.position; break; default: particlePosition = particle.position; break; } ... } 

模拟ParticleSystemRenderer属性


现在,我们实现了ParticleSystemRenderer功能的一部分。 即, RenderModeSortModePivot的属性。

渲染器


我们将自己限制在以下事实:粒子将始终仅位于画布的平面中。 因此,我们仅实现两个值: BillboardStretchedBillboard
让我们为此创建枚举CanvasParticleSystemRenderMode

 public enum CanvasParticleSystemRenderMode { Billboard = 0, StretchedBillboard = 1 } 

 public CanvasParticleSystemRenderMode RenderMode; public float SpeedScale = 0f; public float LengthScale = 1f; protected override void OnPopulateMesh(VertexHelper vh) { ... Quaternion rotation; switch (RenderMode) { case CanvasParticleSystemRenderMode.Billboard: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; case CanvasParticleSystemRenderMode.StretchedBillboard: rotation = Quaternion.LookRotation(Vector3.forward, particle.totalVelocity); float speed = particle.totalVelocity.magnitude; particleSize = Vector3.Scale(particleSize, new Vector3(LengthScale + speed * SpeedScale, 1f, 1f)); rotation *= Quaternion.AngleAxis(90, Vector3.forward); break; default: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; } ... } 

如果选择StretchedBillboard参数,则粒子大小将取决于LengthScaleSpeedScale参数 ,并且其旋转将仅沿运动方向进行。

图片

排序方式


同样,创建CanvasParticlesSortMode枚举。 并且我们仅根据粒子寿命进行分类。

 public enum CanvasParticlesSortMode { None = 0, OldestInFront = 1, YoungestInFront = 2 } 

 public CanvasParticlesSortMode SortMode; 

为了进行排序,我们需要存储有关粒子寿命的数据,这些数据将存储在变量_particleElapsedLifetime中 。 排序是使用Array.Sort方法实现的。

 private float[] _particleElapsedLifetime; protected override void Awake() { ... _particles = new ParticleSystem.Particle[maxCount]; _particleElapsedLifetime = new float[maxCount]; } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) _particleElapsedLifetime[i] = _particles[i].startLifetime - _particles[i].remainingLifetime; switch (SortMode) { case CanvasParticlesSortMode.None: break; case CanvasParticlesSortMode.OldestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount,Comparer<float>.Default); Array.Reverse(_particles, 0, particlesCount); break; case CanvasParticlesSortMode.YoungestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount, Comparer<float>.Default); break; } ... } 

枢轴


创建一个“ 数据透视”字段以偏移粒子的中心点。

 public Vector3 Pivot = Vector3.zero; 

在计算顶点位置时,我们将这个值相加。

 Vector3 cornerPosition = Vector3.Scale(particleSize, _quadCorners[j] + Pivot); Vector3 vertexPosition = rotation * cornerPosition + particlePosition; vertexPosition.z = 0; 

可调尺寸


如果粒子系统所连接的元素没有固定的大小,或者它们可以在运行时更改,那么最好调整粒子系统的大小。 让我们制作源- 形状与元素的大小成比例。

调整RectTransform组件的大小时,将调用OnRectTransformDimensionsChange方法。 我们通过对形状进行比例更改以适应RectTransform的尺寸来重新定义此方法。

首先,为RectTransform组件和ShapeModule模块创建变量。 若要禁用形状缩放,请创建ScaleShapeByRectTransform变量。

另外,应在激活组件以设置其初始比例时执行比例缩放。

 private RectTransform _rectTransform; private ParticleSystem.ShapeModule _shape; public bool ScaleShapeByRectTransform; protected override void Awake() { ... _rectTransform = GetComponent<RectTransform>(); _shape = _particleSystem.shape; ... } protected override void OnEnable() { base.OnEnable(); ScaleShape(); } protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); ScaleShape(); } protected void ScaleShape() { if (!ScaleShapeByRectTransform) return; Rect rect = _rectTransform.rect; var scale = Quaternion.Euler(_shape.rotation) * new Vector3(rect.width, rect.height, 0); scale = new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)); _shape.scale = scale; } 

在计算时,值得考虑Shape的旋转。 最终结果的值必须取模,因为它们可能变为负数,这将影响粒子的运动方向。

要测试操作,请运行带有附加的粒子系统的RectTransform调整大小的动画。

图片

初始化


为了使脚本在编辑器中正确执行并避免在调用OnRectTransformDimensionsChange方法时出错 ,我们将变量的初始化放在单独的方法中。 并将其调用添加到OnPopulateMeshOnRectTransformDimensionsChange方法

ExecuteInEditMode
无需指定ExecuteInEditMode属性,因为 图形已经实现了此行为,并且脚本在编辑器中执行。

 private bool _initialized; protected void Initialize() { if (_initialized) return; _initialized = true; _rectTransform = GetComponent<RectTransform>(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _textureSheetAnimation = _particleSystem.textureSheetAnimation; _shape = _particleSystem.shape; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; _particleSystemRenderer.material = null; var maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; _particlesLifeProgress = new float[maxCount]; _particleRemainingLifetime = new float[maxCount]; } protected override void Awake() { base.Awake(); Initialize(); } protected override void OnPopulateMesh(VertexHelper vh) { Initialize(); ... } protected override void OnRectTransformDimensionsChange() { #if UNITY_EDITOR Initialize(); #endif ... } 

可以早于Awake 调用OnRectTransformDimensionsChange方法。 因此,每次调用它时,都必须初始化变量。

性能与优化


与使用ParticleSystemRenderer相比 ,这种粒子渲染要贵一些,后者需要更谨慎的使用,尤其是在移动设备上。
还值得注意的是,如果至少一个Canvas元素被标记为Dirty ,这将导致重新计算整个Canvas几何形状并生成新的渲染命令。 如果UI包含很多复杂的几何图形及其计算,则值得将其拆分为几个嵌入式画布。

图片

PS:所有源代码和演示都是git link
该文章大约在一年前发布,因为它要求在UI中使用ParticleSystem。 当时,我没有找到类似的解决方案,并且可用的解决方案对于当前任务并不是最佳的。 但是,在本文发表前两天,在收集材料的同时,我意外地使用Graphic.OnPopulateMesh方法找到了类似的解决方案。 因此,我认为有必要指定到存储库链接

Source: https://habr.com/ru/post/zh-CN464961/


All Articles