你好 本文旨在使用在
Canvas'e中可视化粒子系统的组件示例,在UI中创建自己的可视组件。
该信息对于在用户界面中实现各种效果非常有用,也可以用于生成网格或对其进行优化。
一些理论或从何处开始创建组件
Unity UI的基础是
Canvas 。 根据UI元素的内部层次结构,渲染系统使用它来显示“多层”几何图形。
任何可视用户界面组件都必须从
Graphic类(或其派生的
MaskableGraphic类)继承,该类将所有必需的数据传递到
CanvasRenderer组件以进行渲染。 数据是在
OnPopulateMesh方法中创建的,每次在组件需要更新其几何形状时(例如,在调整元素大小时)都会调用该方法。
VertexHelper作为参数传递,这有助于为UI生成网格。
组件创建
基数
我们通过创建一个从
MaskableGraphic类继承的
UIParticleSystem脚本开始实现。
MaskableGraphic是
Graphic类的派生类,此外还提供使用蒙版的工作。 重写
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组件,因此其他组件
UIParticleSystem和
CanvasRenderer将负责创建网格并将其渲染到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 [] _quadCorners和
Vector2 [] _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功能的一部分。 即,
RenderMode ,
SortMode ,
Pivot的属性。
渲染器
我们将自己限制在以下事实:粒子将始终仅位于画布的平面中。 因此,我们仅实现两个值:
Billboard和
StretchedBillboard 。
让我们
为此创建枚举
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参数,则粒子大小将取决于
LengthScale和
SpeedScale参数 ,并且其旋转将仅沿运动方向进行。

排序方式
同样,创建
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方法时
出错 ,我们将变量的初始化放在单独的方法中。 并将其调用添加到
OnPopulateMesh和
OnRectTransformDimensionsChange方法 。
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方法找到了类似的解决方案。 因此,我认为有必要指定
到存储库的
链接 。