0


Unity引擎在UI上渲染粒子播放

  大家好,我是阿赵。
  在UI上面显示粒子特效,如果把粒子系统直接拖到Canvas里面,会存在很多问题,比如层级问题、裁剪问题等。这里分享一种用MaskableGraphic和UIVertex来显示粒子特效的方法。

一、 MaskableGraphic和UIVertex简单显示原理

1、简单例子

  在介绍MaskableGraphic和UIVertex是什么之前,先来运行一段代码:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
  5. public class UIVertexTest : MaskableGraphic
  6. {
  7. private UIVertex[] _quad = new UIVertex[4];
  8. // Start is called before the first frame update
  9. private new IEnumerator Start(){
  10. _quad[0]= UIVertex.simpleVert;
  11. _quad[0].color = Color.green;
  12. _quad[0].uv0 = new Vector2(0, 0);
  13. _quad[0].position = new Vector3(-100, 0);
  14. _quad[1]= UIVertex.simpleVert;
  15. _quad[1].color = Color.red;
  16. _quad[1].uv0 = new Vector2(0, 1);
  17. _quad[1].position = new Vector3(-100, 200);
  18. _quad[2]= UIVertex.simpleVert;
  19. _quad[2].color = Color.black;
  20. _quad[2].uv0 = new Vector2(1, 1);
  21. _quad[2].position = new Vector3(100, 200);
  22. _quad[3]= UIVertex.simpleVert;
  23. _quad[3].color = Color.blue;
  24. _quad[3].uv0 = new Vector2(1, 0);
  25. _quad[3].position = new Vector3(100, 0);
  26. yield return null;}
  27. // Update is called once per frame
  28. void Update(){}
  29. protected override void OnPopulateMesh(VertexHelper vh){
  30. vh.Clear();
  31. vh.AddUIVertexQuad(_quad);}}

  在Canvas里面新建一个空的GameObject,然后把脚本挂上去,运行,会得到这样的结果:
在这里插入图片描述

  可以试试层叠或是用Mask做裁剪,发现都没有问题:
在这里插入图片描述
在这里插入图片描述

2、代码解释

  这里用到了几个东西,是要说明一下的:

1. MaskableGraphic

  可以留意到,上面的代码的类并不是继承MonoBehaviour,而是继承了MaskableGraphic。
  MaskableGraphic 继承自 Graphic,并通过 RectMask2D 和 Mask 实现 “可遮罩的图形”。如果觉得复杂,可以简单这么理解,继承MaskableGraphic 的类,挂在Canvas下的对象上时,  这个对象会变成一个类似Image的对象,可以在上面绘制自己想要的东西。
在这里插入图片描述

  继承了MaskableGraphic 之后,一般来说就会出现这些参数了,比如颜色、材质球、是否可以成为射线的目标、是否可遮罩等。是不是和Image很像?

2. UIVertex

  在理解UIVertex之前,要先对Vertex有所了解。一个3D模型能显示出来,最基础需要2点:
(1) 顶点,比如一个三角形需要3个顶点,顶点包括一般包括坐标、颜色、UV坐标之类的信息。
(2) 索引,为什么三个点能构成一个三角形,是因为有索引,比如三个顶点1、2、3,组成了一个  三角形,那么如果是四个顶点1、2、3、4,可以组成2个三角形,可能是1、2、3一组,1、3、4一组。
  这上面的顶点,就是Vertex了。那么UIVertex就很好理解了,它也是顶点,但只是用在UI上的顶点,更具体一点的,就是在MaskableGraphic这个“画纸”上面绘制图形的顶点。
在这里插入图片描述

  具体看一下UIVertex这个类,里面包含的参数并不多,有顶点的位置、法线方向、切线方向、颜色,还有uv0-uv3这么4组UV坐标。
  有了顶点信息,我们就可以自己绘制图形了。

3. OnPopulateMesh

  这个方法是MaskableGraphic里面的方法,我们可以通过override重写它的逻辑。这个方法调用的时机是MaskableGraphic里面的顶点发生改变时,具体一点,比如OnEnable、需要重新生成顶点、改变顶点的颜色、改变MaskableGraphic使用的材质球,之类。
  如果想在没有顶点改变的情况下也调用这个方法,可以通过调用SetAllDirty()方法,也会强制的执行OnPopulateMesh方法。
  这个例子里面,由于我只是绘制一个矩形,也不需要修改,所以我并没有调用SetAllDirty方法,也就是说,除了在一开始的时候绘制了一次矩形,后面实际上这个方法是不会再次调用,除非我手动去修改颜色和材质球。

4. VertexHelper

  作为OnPopulateMesh方法的传入参数VertexHelper是管理了当前MaskableGraphic 这张“画纸”上面的所有顶点。
在这里插入图片描述

  看一下VertexHelper所提供的方法,可以看出,我们可以对MaskableGraphic 添加点、添加三角形、添加四边形、添加整个网格、清理等操作。
  我这个例子,使用了AddUIVertexQuad方法,也就是添加一个四边形。一个四边形是由4个顶点组成2个三角形实现的。每次调用OnPopulateMesh方法的时候,需要先把VertexHelper调用Clear方法清空一下,再重新添加顶点或者三角形,不然会重复添加,越来越多。

二、 通过获取大小说明position的计算

  上面的例子比较简单,赋予了四个顶点固定的UV坐标,不同的颜色,然后position是相对于GameObject自己的坐标的相对坐标,写死了宽度是-100到100,高度是0到200,所以整个矩形是往上偏的。
  这次稍微做复杂一点点,我需要读取RectTransform里面的大小来改变四个顶点的位置,代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
  5. public class UIVertexTest : MaskableGraphic
  6. {
  7. private UIVertex[] _quad = new UIVertex[4];
  8. // Start is called before the first frame update
  9. private new IEnumerator Start(){
  10. UpdateQuad();
  11. yield return null;}
  12. private void UpdateQuad(){
  13. Rect rect = gameObject.GetComponent<RectTransform>().rect;
  14. _quad[0]= UIVertex.simpleVert;
  15. _quad[0].color = Color.green;
  16. _quad[0].uv0 = new Vector2(0, 0);
  17. _quad[0].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y - rect.height / 2, 0);
  18. _quad[1]= UIVertex.simpleVert;
  19. _quad[1].color = Color.red;
  20. _quad[1].uv0 = new Vector2(0, 1);
  21. _quad[1].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y + rect.height / 2, 0);
  22. _quad[2]= UIVertex.simpleVert;
  23. _quad[2].color = Color.black;
  24. _quad[2].uv0 = new Vector2(1, 1);
  25. _quad[2].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y + rect.height / 2, 0);
  26. _quad[3]= UIVertex.simpleVert;
  27. _quad[3].color = Color.blue;
  28. _quad[3].uv0 = new Vector2(1, 0);
  29. _quad[3].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y - rect.height / 2, 0);}
  30. // Update is called once per frame
  31. void Update(){}
  32. protected override void OnPopulateMesh(VertexHelper vh){
  33. vh.Clear();
  34. UpdateQuad();
  35. vh.AddUIVertexQuad(_quad);}}

  现在改变RectTransform里面的宽高
在这里插入图片描述

  可以看到绘制出来的矩形也跟着变化了。
在这里插入图片描述

这次的代码修改主要有:

  1. 把组装顶点的方法从start里面提取出来,封了一个UpdateQuad方法
  2. 通过获取RectTransform的rect,来计算顶点的实际位置。

三、 材质贴图的应用

  只是显示顶点颜色有点单调,这次试试绘制图片。由于之前组建四个顶点的时候,就已经设置了uv0,这个uv0是根据顶点的位置设置了从0,0到1,1四个角的uv坐标,所以把图片赋予进去,按道理是可以直接把图片完整铺满整个矩形的。
  这里需要注意,由于是绘制在UI上的图片,所以需要配合着UI类型的Shader才能正确显示。这里我写了一个最简单的显示图片采用的UI类shader:
在这里插入图片描述

  直接把材质球拖进去就行:
在这里插入图片描述

  图片就能正常显示了。
在这里插入图片描述

  当然,一般我们不会这样拖材质球去使用,所以我在代码里面暴露一个材质球参数,用于修改材质球。

  1. public Material curMat;

  然后在Update里面,调用一个CheckMatChange的方法,检查当前的MaskableGraphic的material如果不等于我指定的材质球,就会设置材质球。material是MaskableGraphic本身的变量,不需要额外声明的。

void Update()
{
CheckMatChange();
}

private void CheckMatChange()
{
if(material!=curMat)
{
material = curMat;
}
}

  还有一个值得注意的地方是,我这里也没有调用SetAllDirty(),但只要修改材质球,就立刻生效了。这是因为,材质球改变,也是调用OnPopulateMesh方法的条件之一,所以不需要其他操作,单纯修改材质球,就已经会调用一次OnPopulateMesh了。
完整代码:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
  5. public class UIVertexTest : MaskableGraphic
  6. {
  7. private UIVertex[] _quad = new UIVertex[4];
  8. public Material curMat;
  9. // Start is called before the first frame update
  10. private new IEnumerator Start(){
  11. UpdateQuad();
  12. yield return null;}
  13. private void UpdateQuad(){
  14. Rect rect = gameObject.GetComponent<RectTransform>().rect;
  15. _quad[0]= UIVertex.simpleVert;
  16. _quad[0].color = Color.green;
  17. _quad[0].uv0 = new Vector2(0, 0);
  18. _quad[0].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y - rect.height / 2, 0);
  19. _quad[1]= UIVertex.simpleVert;
  20. _quad[1].color = Color.red;
  21. _quad[1].uv0 = new Vector2(0, 1);
  22. _quad[1].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y + rect.height / 2, 0);
  23. _quad[2]= UIVertex.simpleVert;
  24. _quad[2].color = Color.black;
  25. _quad[2].uv0 = new Vector2(1, 1);
  26. _quad[2].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y + rect.height / 2, 0);
  27. _quad[3]= UIVertex.simpleVert;
  28. _quad[3].color = Color.blue;
  29. _quad[3].uv0 = new Vector2(1, 0);
  30. _quad[3].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y - rect.height / 2, 0);}
  31. // Update is called once per frame
  32. void Update(){
  33. CheckMatChange();}
  34. private void CheckMatChange(){
  35. if(material!=curMat){
  36. material = curMat;}}
  37. protected override void OnPopulateMesh(VertexHelper vh){
  38. vh.Clear();
  39. UpdateQuad();
  40. vh.AddUIVertexQuad(_quad);}}

四、 模拟粒子

  通过上面的说明,按道理应该大概了解了怎样通过UIVertex在MaskableGraphic上绘制图形了,接下来就进入主题:

1、 简单模拟粒子

先上代码:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;
  5. using static UnityEngine.ParticleSystem;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
  6. public class UIVertexTest : MaskableGraphic
  7. {
  8. private UIVertex[] _quad = new UIVertex[4];
  9. public Material curMat;
  10. public ParticleSystem particleSys;
  11. private ParticleSystemRenderer particleSysRender;
  12. private ParticleSystem.MainModule mainModule;
  13. private Particle[] particles;
  14. // Start is called before the first frame update
  15. private new IEnumerator Start(){
  16. if(particleSys == null){
  17. particleSys = gameObject.GetComponent<ParticleSystem>();
  18. if(particleSys!=null){
  19. particleSysRender = particleSys.GetComponent<ParticleSystemRenderer>();
  20. particleSysRender.enabled =false;
  21. mainModule = particleSys.main;}}
  22. yield return null;}
  23. private void UpdateQuad(ParticleSystem ps, Particle p){
  24. Color curCol = p.GetCurrentColor(ps);
  25. float size = p.GetCurrentSize(ps) * 0.5f;
  26. Vector2 position =(mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
  27. float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
  28. position /= scale;
  29. size /= scale;
  30. _quad[0]= UIVertex.simpleVert;
  31. _quad[0].color = curCol;
  32. _quad[0].uv0 = new Vector2(0, 0);
  33. _quad[0].position = new Vector3(position.x - size, position.y - size);
  34. _quad[1]= UIVertex.simpleVert;
  35. _quad[1].color = curCol;
  36. _quad[1].uv0 = new Vector2(0, 1);
  37. _quad[1].position = new Vector3(position.x - size, position.y + size);
  38. _quad[2]= UIVertex.simpleVert;
  39. _quad[2].color = curCol;
  40. _quad[2].uv0 = new Vector2(1, 1);
  41. _quad[2].position = new Vector3(position.x + size, position.y + size);
  42. _quad[3]= UIVertex.simpleVert;
  43. _quad[3].color = curCol;
  44. _quad[3].uv0 = new Vector2(1, 0);
  45. _quad[3].position = new Vector3(position.x + size, position.y - size);}
  46. // Update is called once per frame
  47. void Update(){
  48. CheckMatChange();
  49. CheckParticle();}
  50. private void CheckMatChange(){
  51. if(material!=curMat){
  52. material = curMat;}}
  53. private void CheckParticle(){if(particleSys != null){
  54. SetAllDirty();}}
  55. protected override void OnPopulateMesh(VertexHelper vh){
  56. vh.Clear();if(particleSys == null){return;}
  57. if(particles == null){
  58. particles = new Particle[500];}
  59. int count = particleSys.GetParticles(particles);
  60. if(count ==0){return;}
  61. for(int i =0;i<count;i++){
  62. Particle p = particles[i];
  63. UpdateQuad(particleSys, p);
  64. vh.AddUIVertexQuad(_quad);}}}

  现在把一个简单的粒子系统放在Canvas下面,然后把脚本挂到粒子系统上。给它赋予一个UI类的shader。这时候运行,可以看到粒子系统上面的renderer是关闭状态的:
在这里插入图片描述

  但UI上面却看到了粒子效果:
在这里插入图片描述

  而且这个粒子可以裁剪、可以随意重叠:
在这里插入图片描述

在这里插入图片描述

接下来说一下原理:

  1. 从粒子系统里面,获取粒子的数量,然后做一个循环,每一个粒子绘制一个四边形,然后计算粒子的大小和位置。
  2. 为了怕绘制得太多,所以获取粒子的时候,数组最大值只设置了500: particles = new Particle[500]; 当然,这样做可能也不太好,因为如果粒子系统本身发射的粒子太多,只获取500个可能表现会有问题,所以在设计粒子的时候,可以在粒子系统上面设置一下粒子的最大发射数量,保证粒子在有限数量内,效果是正常的。
  3. 计算粒子四个顶点的位置 每个粒子的位置和当前大小是可以获得的,但位置需要乘以自身的Transform的逆矩阵来计算
  1. Vector2 position =(mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
  1. 粒子的大小问题 由于粒子是放在Canvas里面的,所以实际上它是经过了Canvas的缩放的。所以需要获取Canvas的缩放,然后给计算的顶点做一个缩放

2、 模拟粒子高级效果

  刚才的粒子模拟,是最基础的,每个粒子都是只计算了位移和缩放,没有计算旋转,还有,没有支持粒子系统本身的一些特殊效果,比如序列帧的播放功能。下面补全一下:

1. 考虑旋转

修改UpdateQuad方法,把position的设置放在最后面,判断一下旋转,并做一个偏移:

  1. private void UpdateQuad(ParticleSystem ps, Particle p){
  2. Color curCol = p.GetCurrentColor(ps);
  3. float size = p.GetCurrentSize(ps) * 0.5f;
  4. float rotation =-p.rotation * Mathf.Deg2Rad;
  5. float rotation90 = rotation + Mathf.PI / 2;
  6. Vector2 position =(mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
  7. float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
  8. position /= scale;
  9. size /= scale;
  10. _quad[0]= UIVertex.simpleVert;
  11. _quad[0].color = curCol;
  12. _quad[0].uv0 = new Vector2(0, 0);
  13. _quad[1]= UIVertex.simpleVert;
  14. _quad[1].color = curCol;
  15. _quad[1].uv0 = new Vector2(0, 1);
  16. _quad[2]= UIVertex.simpleVert;
  17. _quad[2].color = curCol;
  18. _quad[2].uv0 = new Vector2(1, 1);
  19. _quad[3]= UIVertex.simpleVert;
  20. _quad[3].color = curCol;
  21. _quad[3].uv0 = new Vector2(1, 0);if(rotation ==0){
  22. Vector4 posOffset = new Vector4();
  23. posOffset.x = position.x - size;
  24. posOffset.y = position.y - size;
  25. posOffset.z = position.x + size;
  26. posOffset.w = position.y + size;
  27. _quad[0].position = new Vector3(posOffset.x,posOffset.y);
  28. _quad[1].position = new Vector3(posOffset.x, posOffset.w);
  29. _quad[2].position = new Vector3(posOffset.z, posOffset.w);
  30. _quad[3].position = new Vector3(posOffset.z, posOffset.y);}else{
  31. Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
  32. Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
  33. _quad[0].position = position - right - up;
  34. _quad[1].position = position - right + up;
  35. _quad[2].position = position + right + up;
  36. _quad[3].position = position + right - up;}}

2. 考虑序列帧动画

  粒子系统本身自带播放序列帧的功能的,比如我做了这么一张序列帧图:
在这里插入图片描述

  然后在Texture Sheet Animation里面指定一下横竖列的格子数量,按道理粒子系统就会从1到16那样按顺序播放序列帧动画:
在这里插入图片描述

在代码里面,要这样模拟:

  1. private void UpdateQuad(ParticleSystem ps, Particle p){
  2. Color curCol = p.GetCurrentColor(ps);
  3. float size = p.GetCurrentSize(ps) * 0.5f;
  4. float rotation =-p.rotation * Mathf.Deg2Rad;
  5. float rotation90 = rotation + Mathf.PI / 2;
  6. Vector2 position =(mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
  7. float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
  8. position /= scale;
  9. size /= scale;
  10. Vector4 particleUV = imageUV;
  11. //计算序列帧动画每一个粒子的uv
  12. if(ps.textureSheetAnimation.enabled ==true){
  13. TextureSheetAnimationModule textureSheetAnimation = ps.textureSheetAnimation;
  14. float frameProgress =1 - (p.remainingLifetime / p.startLifetime);if(textureSheetAnimation.frameOverTime.curveMin != null){
  15. frameProgress = textureSheetAnimation.frameOverTime.curveMin.Evaluate(1 - (p.remainingLifetime / p.startLifetime));}elseif(textureSheetAnimation.frameOverTime.curve != null){
  16. frameProgress = textureSheetAnimation.frameOverTime.curve.Evaluate(1 - (p.remainingLifetime / p.startLifetime));}elseif(textureSheetAnimation.frameOverTime.constant >0){
  17. frameProgress = textureSheetAnimation.frameOverTime.constant - (p.remainingLifetime / p.startLifetime);}
  18. frameProgress = Mathf.Repeat(frameProgress * textureSheetAnimation.cycleCount, 1);
  19. int frame =0;
  20. int textureSheetAnimationFrames = textureSheetAnimation.numTilesX * textureSheetAnimation.numTilesY;
  21. Vector2 textureSheetAnimationFrameSize = new Vector2(1f / textureSheetAnimation.numTilesX, 1f / textureSheetAnimation.numTilesY);
  22. switch (textureSheetAnimation.animation){case ParticleSystemAnimationType.WholeSheet:
  23. frame = Mathf.FloorToInt(frameProgress * textureSheetAnimationFrames);break;case ParticleSystemAnimationType.SingleRow:
  24. frame = Mathf.FloorToInt(frameProgress * textureSheetAnimation.numTilesX);
  25. int row = textureSheetAnimation.rowIndex;
  26. // if(textureSheetAnimation.useRandomRow){ // FIXME - is this handled internally by rowIndex?
  27. // row = Random.Range(0, textureSheetAnimation.numTilesY, using: particle.randomSeed);
  28. // }
  29. frame += row * textureSheetAnimation.numTilesX;break;}
  30. frame %= textureSheetAnimationFrames;
  31. particleUV.x =(frame % textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.x;
  32. particleUV.y =1-Mathf.FloorToInt(frame / textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.y- textureSheetAnimationFrameSize.y;
  33. particleUV.z = particleUV.x + textureSheetAnimationFrameSize.x;
  34. particleUV.w = particleUV.y + textureSheetAnimationFrameSize.y;}
  35. _quad[0]= UIVertex.simpleVert;
  36. _quad[0].color = curCol;
  37. _quad[0].uv0 = new Vector2(particleUV.x, particleUV.y);
  38. _quad[1]= UIVertex.simpleVert;
  39. _quad[1].color = curCol;
  40. _quad[1].uv0 = new Vector2(particleUV.x, particleUV.w);
  41. _quad[2]= UIVertex.simpleVert;
  42. _quad[2].color = curCol;
  43. _quad[2].uv0 = new Vector2(particleUV.z, particleUV.w);
  44. _quad[3]= UIVertex.simpleVert;
  45. _quad[3].color = curCol;
  46. _quad[3].uv0 = new Vector2(particleUV.z, particleUV.y);if(rotation ==0){
  47. Vector4 posOffset = new Vector4();
  48. posOffset.x = position.x - size;
  49. posOffset.y = position.y - size;
  50. posOffset.z = position.x + size;
  51. posOffset.w = position.y + size;
  52. _quad[0].position = new Vector3(posOffset.x,posOffset.y);
  53. _quad[1].position = new Vector3(posOffset.x, posOffset.w);
  54. _quad[2].position = new Vector3(posOffset.z, posOffset.w);
  55. _quad[3].position = new Vector3(posOffset.z, posOffset.y);}else{
  56. Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
  57. Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
  58. _quad[0].position = position - right - up;
  59. _quad[1].position = position - right + up;
  60. _quad[2].position = position + right + up;
  61. _quad[3].position = position + right - up;}}

同样是修改UpdateQuad方法。这次要修改的是设置顶点的UV0的过程。每一个粒子,都可以通过获得它的生命周期,然后计算应该播放到序列图的第几帧的图片,然后根据这个第几帧,再计算出当前的例子的UV坐标应该是什么。
这里有个规则的问题,uv坐标从左下角开始是(0,0),右上角是(1,1),但我的序列图四左上角开始是1,右下角是16,所以在计算y坐标的时候,我做了一个反转的操作:
particleUV.y = 1-Mathf.FloorToInt(frame / textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.y- textureSheetAnimationFrameSize.y;
这时候播放,会看到序列帧的图片正常的播放了:
在这里插入图片描述

同样是可以任意重叠和裁剪的。

完整:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;
  5. using static UnityEngine.ParticleSystem;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
  6. public class UIVertexTest : MaskableGraphic
  7. {
  8. private UIVertex[] _quad = new UIVertex[4];
  9. public Material curMat;
  10. public ParticleSystem particleSys;
  11. private ParticleSystemRenderer particleSysRender;
  12. private ParticleSystem.MainModule mainModule;
  13. private Particle[] particles;
  14. private Vector4 imageUV = new Vector4(0, 0, 1, 1);
  15. // Start is called before the first frame update
  16. private new IEnumerator Start(){
  17. if(particleSys == null){
  18. particleSys = gameObject.GetComponent<ParticleSystem>();
  19. if(particleSys!=null){
  20. particleSysRender = particleSys.GetComponent<ParticleSystemRenderer>();
  21. particleSysRender.enabled =false;
  22. mainModule = particleSys.main;}}
  23. yield return null;}
  24. private void UpdateQuad(ParticleSystem ps, Particle p){
  25. Color curCol = p.GetCurrentColor(ps);
  26. float size = p.GetCurrentSize(ps) * 0.5f;
  27. float rotation =-p.rotation * Mathf.Deg2Rad;
  28. float rotation90 = rotation + Mathf.PI / 2;
  29. Vector2 position =(mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
  30. float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
  31. position /= scale;
  32. size /= scale;
  33. Vector4 particleUV = imageUV;
  34. //计算序列帧动画每一个粒子的uv
  35. if(ps.textureSheetAnimation.enabled ==true){
  36. TextureSheetAnimationModule textureSheetAnimation = ps.textureSheetAnimation;
  37. float frameProgress =1 - (p.remainingLifetime / p.startLifetime);if(textureSheetAnimation.frameOverTime.curveMin != null){
  38. frameProgress = textureSheetAnimation.frameOverTime.curveMin.Evaluate(1 - (p.remainingLifetime / p.startLifetime));}elseif(textureSheetAnimation.frameOverTime.curve != null){
  39. frameProgress = textureSheetAnimation.frameOverTime.curve.Evaluate(1 - (p.remainingLifetime / p.startLifetime));}elseif(textureSheetAnimation.frameOverTime.constant >0){
  40. frameProgress = textureSheetAnimation.frameOverTime.constant - (p.remainingLifetime / p.startLifetime);}
  41. frameProgress = Mathf.Repeat(frameProgress * textureSheetAnimation.cycleCount, 1);
  42. int frame =0;
  43. int textureSheetAnimationFrames = textureSheetAnimation.numTilesX * textureSheetAnimation.numTilesY;
  44. Vector2 textureSheetAnimationFrameSize = new Vector2(1f / textureSheetAnimation.numTilesX, 1f / textureSheetAnimation.numTilesY);
  45. switch (textureSheetAnimation.animation){case ParticleSystemAnimationType.WholeSheet:
  46. frame = Mathf.FloorToInt(frameProgress * textureSheetAnimationFrames);break;case ParticleSystemAnimationType.SingleRow:
  47. frame = Mathf.FloorToInt(frameProgress * textureSheetAnimation.numTilesX);
  48. int row = textureSheetAnimation.rowIndex;
  49. // if(textureSheetAnimation.useRandomRow){ // FIXME - is this handled internally by rowIndex?
  50. // row = Random.Range(0, textureSheetAnimation.numTilesY, using: particle.randomSeed);
  51. // }
  52. frame += row * textureSheetAnimation.numTilesX;break;}
  53. frame %= textureSheetAnimationFrames;
  54. particleUV.x =(frame % textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.x;
  55. particleUV.y =1-Mathf.FloorToInt(frame / textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.y- textureSheetAnimationFrameSize.y;
  56. particleUV.z = particleUV.x + textureSheetAnimationFrameSize.x;
  57. particleUV.w = particleUV.y + textureSheetAnimationFrameSize.y;}
  58. _quad[0]= UIVertex.simpleVert;
  59. _quad[0].color = curCol;
  60. _quad[0].uv0 = new Vector2(particleUV.x, particleUV.y);
  61. _quad[1]= UIVertex.simpleVert;
  62. _quad[1].color = curCol;
  63. _quad[1].uv0 = new Vector2(particleUV.x, particleUV.w);
  64. _quad[2]= UIVertex.simpleVert;
  65. _quad[2].color = curCol;
  66. _quad[2].uv0 = new Vector2(particleUV.z, particleUV.w);
  67. _quad[3]= UIVertex.simpleVert;
  68. _quad[3].color = curCol;
  69. _quad[3].uv0 = new Vector2(particleUV.z, particleUV.y);if(rotation ==0){
  70. Vector4 posOffset = new Vector4();
  71. posOffset.x = position.x - size;
  72. posOffset.y = position.y - size;
  73. posOffset.z = position.x + size;
  74. posOffset.w = position.y + size;
  75. _quad[0].position = new Vector3(posOffset.x,posOffset.y);
  76. _quad[1].position = new Vector3(posOffset.x, posOffset.w);
  77. _quad[2].position = new Vector3(posOffset.z, posOffset.w);
  78. _quad[3].position = new Vector3(posOffset.z, posOffset.y);}else{
  79. Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
  80. Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
  81. _quad[0].position = position - right - up;
  82. _quad[1].position = position - right + up;
  83. _quad[2].position = position + right + up;
  84. _quad[3].position = position + right - up;}}
  85. // Update is called once per frame
  86. void Update(){
  87. CheckMatChange();
  88. CheckParticle();}
  89. private void CheckMatChange(){
  90. if(material!=curMat){
  91. material = curMat;}}
  92. private void CheckParticle(){if(particleSys != null){
  93. SetAllDirty();}}
  94. protected override void OnPopulateMesh(VertexHelper vh){
  95. vh.Clear();if(particleSys == null){return;}
  96. if(particles == null){
  97. particles = new Particle[500];}
  98. int count = particleSys.GetParticles(particles);
  99. if(count ==0){return;}
  100. for(int i =0;i<count;i++){
  101. Particle p = particles[i];
  102. UpdateQuad(particleSys, p);
  103. vh.AddUIVertexQuad(_quad);}}}

五、存在问题

  这个方案也不是完美无缺的,暂时来说,我发现一些问题,比如:
1、一般特效里面除了粒子系统,还有拖尾等效果,我这个例子里面没有对拖尾进行计算,其实方法相同,也是获得它当前的拖尾的网格,然后绘制就行
2、粒子系统里面存在很多不同的参数设置,我上面的计算不一定很完整,可能会漏一些东西,需要发现的时候再补充
3、MaskableGraphic里面只设置一个材质球,这个材质球必须是用UI类型的Shader才能正常显示,所以粒子自带的shader不能直接使用,要经过改造。所谓的UI类型shader,比如是这样的:

  1. Shader "UIVertexTex"{
  2. Properties
  3. {[PerRendererData] _MainTex ("Sprite Texture", 2D)="white"{}
  4. _Color ("Tint", Color)=(1,1,1,1)
  5. _StencilComp ("Stencil Comparison", Float)=8
  6. _Stencil ("Stencil ID", Float)=0
  7. _StencilOp ("Stencil Operation", Float)=0
  8. _StencilWriteMask ("Stencil Write Mask", Float)=255
  9. _StencilReadMask ("Stencil Read Mask", Float)=255
  10. _ColorMask ("Color Mask", Float)=15[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float)=0
  11. _TextureSample0("Texture Sample 0", 2D)="white"{}[HideInInspector] _texcoord("", 2D )="white"{}}
  12. SubShader
  13. {
  14. LOD 0
  15. Tags {"Queue"="Transparent""IgnoreProjector"="True""RenderType"="Transparent""PreviewType"="Plane""CanUseSpriteAtlas"="True"}
  16. Stencil
  17. {
  18. Ref [_Stencil]
  19. ReadMask [_StencilReadMask]
  20. WriteMask [_StencilWriteMask]
  21. CompFront [_StencilComp]
  22. PassFront [_StencilOp]
  23. FailFront Keep
  24. ZFailFront Keep
  25. CompBack Always
  26. PassBack Keep
  27. FailBack Keep
  28. ZFailBack Keep
  29. }
  30. Cull Off
  31. Lighting Off
  32. ZWrite Off
  33. ZTest [unity_GUIZTestMode]
  34. Blend SrcAlpha OneMinusSrcAlpha
  35. ColorMask [_ColorMask]
  36. Pass
  37. {
  38. Name "Default"
  39. CGPROGRAM
  40. #pragma vertex vert#pragma fragment frag#pragma target 3.0#include "UnityCG.cginc"#include "UnityUI.cginc"#pragma multi_compile __ UNITY_UI_CLIP_RECT#pragma multi_compile __ UNITY_UI_ALPHACLIP
  41. struct appdata_t
  42. {
  43. float4 vertex : POSITION;
  44. float4 color : COLOR;
  45. float2 texcoord : TEXCOORD0;
  46. UNITY_VERTEX_INPUT_INSTANCE_ID
  47. };
  48. struct v2f
  49. {
  50. float4 vertex : SV_POSITION;
  51. fixed4 color : COLOR;
  52. half2 texcoord : TEXCOORD0;
  53. float4 worldPosition : TEXCOORD1;
  54. UNITY_VERTEX_INPUT_INSTANCE_ID
  55. UNITY_VERTEX_OUTPUT_STEREO
  56. };
  57. uniform fixed4 _Color;
  58. uniform fixed4 _TextureSampleAdd;
  59. uniform float4 _ClipRect;
  60. uniform sampler2D _MainTex;
  61. uniform sampler2D _TextureSample0;
  62. uniform float4 _TextureSample0_ST;
  63. SamplerState sampler_TextureSample0;
  64. v2f vert( appdata_t IN ){
  65. v2f OUT;
  66. UNITY_SETUP_INSTANCE_ID( IN );
  67. UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
  68. UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
  69. OUT.worldPosition = IN.vertex;
  70. OUT.worldPosition.xyz += float3(0, 0, 0);
  71. OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
  72. OUT.texcoord = IN.texcoord;
  73. OUT.color = IN.color * _Color;return OUT;}
  74. fixed4 frag(v2f IN ): SV_Target
  75. {
  76. float2 uv_TextureSample0 = IN.texcoord.xy * _TextureSample0_ST.xy + _TextureSample0_ST.zw;
  77. float4 tex2DNode1 = tex2D( _TextureSample0, uv_TextureSample0 );
  78. clip(0.0);
  79. float4 appendResult3 =(float4(( tex2DNode1 + tex2DNode1 ).rgb , tex2DNode1.a));
  80. half4 color = appendResult3;#ifdef UNITY_UI_CLIP_RECT
  81. color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);#endif#ifdef UNITY_UI_ALPHACLIP
  82. clip (color.a - 0.001);#endifreturn color;}
  83. ENDCG
  84. }}

它要包含UI的蒙版设置,包含UnityUI.cginc,并且做了Rect和Alpha的裁剪

  所以并没有像想象中那么简单,美术特效随便做一个粒子特效,然后挂个脚本,就能随便用在UI上面,最起码,要对Shader进行一定的改造,才能在UI上面正常显示的。

标签: unity ui 游戏引擎

本文转载自: https://blog.csdn.net/liweizhao/article/details/139483846
版权归原作者 阿赵3D 所有, 如有侵权,请联系我们删除。

“Unity引擎在UI上渲染粒子播放”的评论:

还没有评论