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

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

一、 MaskableGraphic和UIVertex简单显示原理

1、简单例子

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
public class UIVertexTest : MaskableGraphic
{
    private UIVertex[] _quad = new UIVertex[4];

    // Start is called before the first frame update
    private new IEnumerator Start()
    {
        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = Color.green;
        _quad[0].uv0 = new Vector2(0, 0);
        _quad[0].position = new Vector3(-100, 0);

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = Color.red;
        _quad[1].uv0 = new Vector2(0, 1);
        _quad[1].position = new Vector3(-100, 200);

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = Color.black;
        _quad[2].uv0 = new Vector2(1, 1);
        _quad[2].position = new Vector3(100, 200);

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = Color.blue;
        _quad[3].uv0 = new Vector2(1, 0);
        _quad[3].position = new Vector3(100, 0);

        yield return null;
    }

    // Update is called once per frame
    void Update()
    {

    }

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();
        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里面的大小来改变四个顶点的位置,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
public class UIVertexTest : MaskableGraphic
{
    private UIVertex[] _quad = new UIVertex[4];
    // Start is called before the first frame update
    private new IEnumerator Start()
    {

        UpdateQuad();
        yield return null;
    }


    private void UpdateQuad()
    {
        Rect rect = gameObject.GetComponent<RectTransform>().rect;
        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = Color.green;
        _quad[0].uv0 = new Vector2(0, 0);
        _quad[0].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y - rect.height / 2, 0);

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = Color.red;
        _quad[1].uv0 = new Vector2(0, 1);
        _quad[1].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y + rect.height / 2, 0);

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = Color.black;
        _quad[2].uv0 = new Vector2(1, 1);
        _quad[2].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y + rect.height / 2, 0);

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = Color.blue;
        _quad[3].uv0 = new Vector2(1, 0);
        _quad[3].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y - rect.height / 2, 0);
    }
    // Update is called once per frame
    void Update()
    {

    }

    protected override void OnPopulateMesh(VertexHelper vh)
    {      
        vh.Clear();
        UpdateQuad();
        vh.AddUIVertexQuad(_quad);
    }
}

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

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

这次的代码修改主要有:

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

三、 材质贴图的应用

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

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

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

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

public Material curMat;

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

void Update()
{
CheckMatChange();
}

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

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
public class UIVertexTest : MaskableGraphic
{
    private UIVertex[] _quad = new UIVertex[4];
    public Material curMat;
    // Start is called before the first frame update
    private new IEnumerator Start()
    {

        UpdateQuad();
        yield return null;
    }


    private void UpdateQuad()
    {
        Rect rect = gameObject.GetComponent<RectTransform>().rect;
        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = Color.green;
        _quad[0].uv0 = new Vector2(0, 0);
        _quad[0].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y - rect.height / 2, 0);

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = Color.red;
        _quad[1].uv0 = new Vector2(0, 1);
        _quad[1].position = new Vector3(rect.center.x - rect.width / 2, rect.center.y + rect.height / 2, 0);

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = Color.black;
        _quad[2].uv0 = new Vector2(1, 1);
        _quad[2].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y + rect.height / 2, 0);

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = Color.blue;
        _quad[3].uv0 = new Vector2(1, 0);
        _quad[3].position = new Vector3(rect.center.x + rect.width / 2, rect.center.y - rect.height / 2, 0);
    }
    // Update is called once per frame
    void Update()
    {
        CheckMatChange();
    }

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

    protected override void OnPopulateMesh(VertexHelper vh)
    {      
        vh.Clear();
        UpdateQuad();
        vh.AddUIVertexQuad(_quad);
    }
}

四、 模拟粒子

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

1、 简单模拟粒子

先上代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using static UnityEngine.ParticleSystem;

[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
public class UIVertexTest : MaskableGraphic
{
    private UIVertex[] _quad = new UIVertex[4];
    public Material curMat;
    public ParticleSystem particleSys;
    private ParticleSystemRenderer particleSysRender;
    private ParticleSystem.MainModule mainModule;
    private Particle[] particles;
    // Start is called before the first frame update
    private new IEnumerator Start()
    {
        if(particleSys == null)
        {
            particleSys = gameObject.GetComponent<ParticleSystem>();
            if(particleSys!=null)
            {
                particleSysRender = particleSys.GetComponent<ParticleSystemRenderer>();
                particleSysRender.enabled = false;
                mainModule = particleSys.main;
            }
        }
        
        yield return null;
    }
    


    private void UpdateQuad(ParticleSystem ps, Particle p)
    {
        Color curCol = p.GetCurrentColor(ps);
        float size = p.GetCurrentSize(ps) * 0.5f;
        Vector2 position = (mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
        float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
        position /= scale;
        size /= scale;
        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = curCol;
        _quad[0].uv0 = new Vector2(0, 0);
        _quad[0].position = new Vector3(position.x - size, position.y - size);

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = curCol;
        _quad[1].uv0 = new Vector2(0, 1);
        _quad[1].position = new Vector3(position.x - size, position.y + size);

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = curCol;
        _quad[2].uv0 = new Vector2(1, 1);
        _quad[2].position = new Vector3(position.x + size, position.y + size);

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = curCol; 
        _quad[3].uv0 = new Vector2(1, 0);
        _quad[3].position = new Vector3(position.x + size, position.y - size);
    }
    // Update is called once per frame
    void Update()
    {
        CheckMatChange();
CheckParticle();
    }

    private void CheckMatChange()
    {
        if(material!=curMat)
        {
            material = curMat;
        }
    }
    private void CheckParticle()
    {
        if (particleSys != null)
        {
            SetAllDirty();
        }
    }
    protected override void OnPopulateMesh(VertexHelper vh)
    {      
        vh.Clear();
        if (particleSys == null)
        {
            return;
        }

        if(particles == null)
        {
            particles = new Particle[500];
        }
        int count = particleSys.GetParticles(particles);
        if(count == 0)
        {
            return;
        }
        for(int i = 0;i<count;i++)
        {
            Particle p = particles[i];
            UpdateQuad(particleSys, p);
            vh.AddUIVertexQuad(_quad);
        }
        
    }
}

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

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

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

在这里插入图片描述

接下来说一下原理:

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

2、 模拟粒子高级效果

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

1. 考虑旋转

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

    private void UpdateQuad(ParticleSystem ps, Particle p)
    {
        Color curCol = p.GetCurrentColor(ps);
        float size = p.GetCurrentSize(ps) * 0.5f;
        float rotation = -p.rotation * Mathf.Deg2Rad;
        float rotation90 = rotation + Mathf.PI / 2;
        Vector2 position = (mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
        float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
        position /= scale;
        size /= scale;
        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = curCol;
        _quad[0].uv0 = new Vector2(0, 0);
        

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = curCol;
        _quad[1].uv0 = new Vector2(0, 1);
        

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = curCol;
        _quad[2].uv0 = new Vector2(1, 1);
        

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = curCol;
        _quad[3].uv0 = new Vector2(1, 0);


        if (rotation == 0)
        {
            Vector4 posOffset = new Vector4();
            posOffset.x = position.x - size;
            posOffset.y = position.y - size;
            posOffset.z = position.x + size;
            posOffset.w = position.y + size;
            _quad[0].position = new Vector3(posOffset.x,posOffset.y);
            _quad[1].position = new Vector3(posOffset.x, posOffset.w);
            _quad[2].position = new Vector3(posOffset.z, posOffset.w);
            _quad[3].position = new Vector3(posOffset.z, posOffset.y);
        }
        else
        {
            Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
            Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
            _quad[0].position = position - right - up;
            _quad[1].position = position - right + up;
            _quad[2].position = position + right + up;
            _quad[3].position = position + right - up;
        }
}

2. 考虑序列帧动画

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

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

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

    private void UpdateQuad(ParticleSystem ps, Particle p)
    {
        Color curCol = p.GetCurrentColor(ps);
        float size = p.GetCurrentSize(ps) * 0.5f;
        float rotation = -p.rotation * Mathf.Deg2Rad;
        float rotation90 = rotation + Mathf.PI / 2;
        Vector2 position = (mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
        float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
        position /= scale;
        size /= scale;


        Vector4 particleUV = imageUV;

        //计算序列帧动画每一个粒子的uv
        if(ps.textureSheetAnimation.enabled == true)
        {
            TextureSheetAnimationModule textureSheetAnimation = ps.textureSheetAnimation;
            float frameProgress = 1 - (p.remainingLifetime / p.startLifetime);

            if (textureSheetAnimation.frameOverTime.curveMin != null)
            {
                frameProgress = textureSheetAnimation.frameOverTime.curveMin.Evaluate(1 - (p.remainingLifetime / p.startLifetime));
            }
            else if (textureSheetAnimation.frameOverTime.curve != null)
            {
                frameProgress = textureSheetAnimation.frameOverTime.curve.Evaluate(1 - (p.remainingLifetime / p.startLifetime));
            }
            else if (textureSheetAnimation.frameOverTime.constant > 0)
            {
                frameProgress = textureSheetAnimation.frameOverTime.constant - (p.remainingLifetime / p.startLifetime);
            }

            frameProgress = Mathf.Repeat(frameProgress * textureSheetAnimation.cycleCount, 1);
            int frame = 0;
            int textureSheetAnimationFrames = textureSheetAnimation.numTilesX * textureSheetAnimation.numTilesY;
            Vector2 textureSheetAnimationFrameSize = new Vector2(1f / textureSheetAnimation.numTilesX, 1f / textureSheetAnimation.numTilesY);
            switch (textureSheetAnimation.animation)
            {

                case ParticleSystemAnimationType.WholeSheet:
                    frame = Mathf.FloorToInt(frameProgress * textureSheetAnimationFrames);
                    break;

                case ParticleSystemAnimationType.SingleRow:
                    frame = Mathf.FloorToInt(frameProgress * textureSheetAnimation.numTilesX);

                    int row = textureSheetAnimation.rowIndex;
                    //                    if (textureSheetAnimation.useRandomRow) { // FIXME - is this handled internally by rowIndex?
                    //                        row = Random.Range(0, textureSheetAnimation.numTilesY, using: particle.randomSeed);
                    //                    }
                    frame += row * textureSheetAnimation.numTilesX;
                    break;

            }

            frame %= textureSheetAnimationFrames;

            particleUV.x = (frame % textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.x;
            particleUV.y = 1-Mathf.FloorToInt(frame / textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.y- textureSheetAnimationFrameSize.y;
            particleUV.z = particleUV.x + textureSheetAnimationFrameSize.x;
            particleUV.w = particleUV.y + textureSheetAnimationFrameSize.y;
        }

        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = curCol;
        _quad[0].uv0 = new Vector2(particleUV.x, particleUV.y);
        

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = curCol;
        _quad[1].uv0 = new Vector2(particleUV.x, particleUV.w);
        

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = curCol;
        _quad[2].uv0 = new Vector2(particleUV.z, particleUV.w);
        

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = curCol;
        _quad[3].uv0 = new Vector2(particleUV.z, particleUV.y);


        if (rotation == 0)
        {
            Vector4 posOffset = new Vector4();
            posOffset.x = position.x - size;
            posOffset.y = position.y - size;
            posOffset.z = position.x + size;
            posOffset.w = position.y + size;
            _quad[0].position = new Vector3(posOffset.x,posOffset.y);
            _quad[1].position = new Vector3(posOffset.x, posOffset.w);
            _quad[2].position = new Vector3(posOffset.z, posOffset.w);
            _quad[3].position = new Vector3(posOffset.z, posOffset.y);
        }
        else
        {
            Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
            Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
            _quad[0].position = position - right - up;
            _quad[1].position = position - right + up;
            _quad[2].position = position + right + up;
            _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;
这时候播放,会看到序列帧的图片正常的播放了:
在这里插入图片描述

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

完整:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using static UnityEngine.ParticleSystem;

[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
public class UIVertexTest : MaskableGraphic
{
    private UIVertex[] _quad = new UIVertex[4];
    public Material curMat;
    public ParticleSystem particleSys;
    private ParticleSystemRenderer particleSysRender;
    private ParticleSystem.MainModule mainModule;
    private Particle[] particles;
    private Vector4 imageUV = new Vector4(0, 0, 1, 1);
    // Start is called before the first frame update
    private new IEnumerator Start()
    {
        if(particleSys == null)
        {
            particleSys = gameObject.GetComponent<ParticleSystem>();
            if(particleSys!=null)
            {
                particleSysRender = particleSys.GetComponent<ParticleSystemRenderer>();
                particleSysRender.enabled = false;
                mainModule = particleSys.main;
            }
        }
        
        yield return null;
    }



    private void UpdateQuad(ParticleSystem ps, Particle p)
    {
        Color curCol = p.GetCurrentColor(ps);
        float size = p.GetCurrentSize(ps) * 0.5f;
        float rotation = -p.rotation * Mathf.Deg2Rad;
        float rotation90 = rotation + Mathf.PI / 2;
        Vector2 position = (mainModule.simulationSpace == ParticleSystemSimulationSpace.Local ? p.position : ps.transform.InverseTransformPoint(p.position));
        float scale = canvas.gameObject.GetComponent<RectTransform>().localScale.x;
        position /= scale;
        size /= scale;


        Vector4 particleUV = imageUV;

        //计算序列帧动画每一个粒子的uv
        if(ps.textureSheetAnimation.enabled == true)
        {
            TextureSheetAnimationModule textureSheetAnimation = ps.textureSheetAnimation;
            float frameProgress = 1 - (p.remainingLifetime / p.startLifetime);

            if (textureSheetAnimation.frameOverTime.curveMin != null)
            {
                frameProgress = textureSheetAnimation.frameOverTime.curveMin.Evaluate(1 - (p.remainingLifetime / p.startLifetime));
            }
            else if (textureSheetAnimation.frameOverTime.curve != null)
            {
                frameProgress = textureSheetAnimation.frameOverTime.curve.Evaluate(1 - (p.remainingLifetime / p.startLifetime));
            }
            else if (textureSheetAnimation.frameOverTime.constant > 0)
            {
                frameProgress = textureSheetAnimation.frameOverTime.constant - (p.remainingLifetime / p.startLifetime);
            }

            frameProgress = Mathf.Repeat(frameProgress * textureSheetAnimation.cycleCount, 1);
            int frame = 0;
            int textureSheetAnimationFrames = textureSheetAnimation.numTilesX * textureSheetAnimation.numTilesY;
            Vector2 textureSheetAnimationFrameSize = new Vector2(1f / textureSheetAnimation.numTilesX, 1f / textureSheetAnimation.numTilesY);
            switch (textureSheetAnimation.animation)
            {

                case ParticleSystemAnimationType.WholeSheet:
                    frame = Mathf.FloorToInt(frameProgress * textureSheetAnimationFrames);
                    break;

                case ParticleSystemAnimationType.SingleRow:
                    frame = Mathf.FloorToInt(frameProgress * textureSheetAnimation.numTilesX);

                    int row = textureSheetAnimation.rowIndex;
                    //                    if (textureSheetAnimation.useRandomRow) { // FIXME - is this handled internally by rowIndex?
                    //                        row = Random.Range(0, textureSheetAnimation.numTilesY, using: particle.randomSeed);
                    //                    }
                    frame += row * textureSheetAnimation.numTilesX;
                    break;

            }

            frame %= textureSheetAnimationFrames;

            particleUV.x = (frame % textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.x;
            particleUV.y = 1-Mathf.FloorToInt(frame / textureSheetAnimation.numTilesX) * textureSheetAnimationFrameSize.y- textureSheetAnimationFrameSize.y;
            particleUV.z = particleUV.x + textureSheetAnimationFrameSize.x;
            particleUV.w = particleUV.y + textureSheetAnimationFrameSize.y;
        }

        _quad[0] = UIVertex.simpleVert;
        _quad[0].color = curCol;
        _quad[0].uv0 = new Vector2(particleUV.x, particleUV.y);
        

        _quad[1] = UIVertex.simpleVert;
        _quad[1].color = curCol;
        _quad[1].uv0 = new Vector2(particleUV.x, particleUV.w);
        

        _quad[2] = UIVertex.simpleVert;
        _quad[2].color = curCol;
        _quad[2].uv0 = new Vector2(particleUV.z, particleUV.w);
        

        _quad[3] = UIVertex.simpleVert;
        _quad[3].color = curCol;
        _quad[3].uv0 = new Vector2(particleUV.z, particleUV.y);


        if (rotation == 0)
        {
            Vector4 posOffset = new Vector4();
            posOffset.x = position.x - size;
            posOffset.y = position.y - size;
            posOffset.z = position.x + size;
            posOffset.w = position.y + size;
            _quad[0].position = new Vector3(posOffset.x,posOffset.y);
            _quad[1].position = new Vector3(posOffset.x, posOffset.w);
            _quad[2].position = new Vector3(posOffset.z, posOffset.w);
            _quad[3].position = new Vector3(posOffset.z, posOffset.y);
        }
        else
        {
            Vector2 right = new Vector2(Mathf.Cos(rotation), Mathf.Sin(rotation)) * size;
            Vector2 up = new Vector2(Mathf.Cos(rotation90), Mathf.Sin(rotation90)) * size;
            _quad[0].position = position - right - up;
            _quad[1].position = position - right + up;
            _quad[2].position = position + right + up;
            _quad[3].position = position + right - up;
        }
    }
    // Update is called once per frame
    void Update()
    {
        CheckMatChange();
CheckParticle();
    }

    private void CheckMatChange()
    {
        if(material!=curMat)
        {
            material = curMat;
        }
    }
    private void CheckParticle()
    {
        if (particleSys != null)
        {
            SetAllDirty();
        }
    }
    protected override void OnPopulateMesh(VertexHelper vh)
    {      
        vh.Clear();
        if (particleSys == null)
        {
            return;
        }

        if(particles == null)
        {
            particles = new Particle[500];
        }
        int count = particleSys.GetParticles(particles);
        if(count == 0)
        {
            return;
        }
        for(int i = 0;i<count;i++)
        {
            Particle p = particles[i];
            UpdateQuad(particleSys, p);
            vh.AddUIVertexQuad(_quad);
        }
        
    }
}

五、存在问题

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

Shader "UIVertexTex"
{
	Properties
	{
		[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
		_Color ("Tint", Color) = (1,1,1,1)
		
		_StencilComp ("Stencil Comparison", Float) = 8
		_Stencil ("Stencil ID", Float) = 0
		_StencilOp ("Stencil Operation", Float) = 0
		_StencilWriteMask ("Stencil Write Mask", Float) = 255
		_StencilReadMask ("Stencil Read Mask", Float) = 255

		_ColorMask ("Color Mask", Float) = 15

		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
		_TextureSample0("Texture Sample 0", 2D) = "white" {}
		[HideInInspector] _texcoord( "", 2D ) = "white" {}

	}

	SubShader
	{
		LOD 0

		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" }
		
		Stencil
		{
			Ref [_Stencil]
			ReadMask [_StencilReadMask]
			WriteMask [_StencilWriteMask]
			CompFront [_StencilComp]
			PassFront [_StencilOp]
			FailFront Keep
			ZFailFront Keep
			CompBack Always
			PassBack Keep
			FailBack Keep
			ZFailBack Keep
		}


		Cull Off
		Lighting Off
		ZWrite Off
		ZTest [unity_GUIZTestMode]
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask [_ColorMask]

		
		Pass
		{
			Name "Default"
		CGPROGRAM
			
			#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
			
			
			
			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 texcoord : TEXCOORD0;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color    : COLOR;
				half2 texcoord  : TEXCOORD0;
				float4 worldPosition : TEXCOORD1;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				UNITY_VERTEX_OUTPUT_STEREO
				
			};
			
			uniform fixed4 _Color;
			uniform fixed4 _TextureSampleAdd;
			uniform float4 _ClipRect;
			uniform sampler2D _MainTex;
			uniform sampler2D _TextureSample0;
			uniform float4 _TextureSample0_ST;
			SamplerState sampler_TextureSample0;

			
			v2f vert( appdata_t IN  )
			{
				v2f OUT;
				UNITY_SETUP_INSTANCE_ID( IN );
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
				UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
				OUT.worldPosition = IN.vertex;
				
				
				OUT.worldPosition.xyz +=  float3( 0, 0, 0 ) ;
				OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

				OUT.texcoord = IN.texcoord;
				
				OUT.color = IN.color * _Color;
				return OUT;
			}

			fixed4 frag(v2f IN  ) : SV_Target
			{
				float2 uv_TextureSample0 = IN.texcoord.xy * _TextureSample0_ST.xy + _TextureSample0_ST.zw;
				float4 tex2DNode1 = tex2D( _TextureSample0, uv_TextureSample0 );
				clip( 0.0 );
				float4 appendResult3 = (float4(( tex2DNode1 + tex2DNode1 ).rgb , tex2DNode1.a));
				
				half4 color = appendResult3;
				
				#ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif
				
				#ifdef UNITY_UI_ALPHACLIP
				clip (color.a - 0.001);
				#endif

				return color;
			}
		ENDCG
		}
	}

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/708745.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Python和Matplotlib绘制复杂数学函数图像

本文介绍了如何使用Python编程语言和Matplotlib库来绘制复杂的数学函数图像。通过引入NumPy库的数学函数,我们可以处理包括指数函数在内的各种复杂表达式。本文详细讲解了如何设置中文字体以确保在图像中正确显示中文标题和标签,并提供了一个完整的代码示例,用户可以通过输入…

【Redis进阶】RDB持久化策略

1. 浅谈持久化 持久化&#xff1a;能够在重启主机/进程的时候&#xff0c;将数据从硬盘中恢复到内存的特性。 持久化相信大家都是不陌生的&#xff0c;毕竟MySQL中事务ACID四大特性中就包含持续性这样的特点&#xff0c;所谓持久化&#xff0c;本质上就是将数据保存在硬盘上&a…

Sping源码(九)—— Bean的初始化(非懒加载)— lookupMethod标签

序言 在继续深入Spring的对象创建流程之前&#xff0c;这篇文章先简单介绍一下lookupMethod标签的用法及作用。 准备的xml 自定义名为methodOverride.xml的配置文件。 <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.s…

【星海随笔】云解决方案学习日志篇(三) 工作原理篇

Filebeat工作原理 Filebeat 是使用 Golang 实现的轻量型日志采集器,也是 Elasticsearch stack 里面的一员。本质上是一个 agent ,可以安装在各个节点上,根据配置读取对应位置的日志,并上报到相应的地方去。 使用了背压敏感协议,因此不会使管道过载。当Logstash数据处理繁忙时,…

张艺兴step新专开启自由驾驶新纪元

张艺兴《Step》新专&#xff0c;开启自由驾驶新纪元&#xff01;当音乐与驾驶相遇&#xff0c;会碰撞出怎样的火花&#xff1f;当实力派艺人张艺兴遇上全新英文专辑《Step》&#xff0c;便为我们解锁了一种前所未有的出行体验&#xff01;这不仅仅是一张音乐专辑&#xff0c;更…

南方cass专业测绘软件下载,南方cass功能强大的cad辅助测绘软件获取!

在测绘领域&#xff0c;南方CASS测绘软件无疑是一颗璀璨的明星&#xff0c;被誉为“全能选手”。这款软件在功能方面表现出了令人赞叹的多样性和专业性&#xff0c;为测绘工作提供了极大的便利。 ​ 首先&#xff0c;南方CASS测绘软件具备强大的数据兼容性&#xff0c;支持多种…

万相台的功能是什么?如何使用万相台?

1.特点&#xff1a; 万相台是一个智能渠道&#xff0c;可控性弱&#xff0c;高转化&#xff0c;人群&关键词是黑盒&#xff1b; 2.场景多&#xff1a; 有拉新快、活动加速、上新快、货品加速、活动加速、多目标直投、全站推等&#xff1b; 3.扣费逻辑&#xff1a;cpc付…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第41课-动态添加3D对象

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第41课-动态添加3D对象 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎…

填报志愿选大学专业,文科生如何选专业?

读文科的同学接触的专业知识相对广泛&#xff0c;往往被认为是“万金油”&#xff0c;他们仿佛什么都能做&#xff0c;但是和专业技能类知识不同&#xff0c;缺乏技术支持&#xff0c;从而使得文科专业的就业方向和前景远远比不上理科专业那么明朗&#xff0c;对于众多文科生而…

机器学习:回顾总结

学了什么 进阶内容 接下来如何学习 找个项目自己练习多读前沿paper 学员分布

【linux网络(三)】HTTP协议详解

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux网络 1. 前言2. 序列化和…

使用QT绘制简单的动态数据折线图

两个核心类时QChart和QLineSeries 下面这个示例代码中&#xff0c;定时器每隔一段时间将曲线图中的数据点向右移动 一个单位&#xff0c;同时调整横坐标轴的范围&#xff0c;实现了一次滚动对应移动一个数据点的效果。 QLineSeries最多容纳40961024个点 #include <QtWidg…

boot整合solr

换了新项目组&#xff0c;技术相对老些&#xff0c;于是用boot框架简单记录下&#xff01; 安装 下载路径&#xff1a;https://solr.apache.org/downloads.html Windows环境 下载solr-8.2.0.zip包并解压缩&#xff0c;以管理员身份打开cmd&#xff0c;执行 solr cmd 命令启…

【开源项目】重庆智慧城市案例~实景三维数字孪生城市CIM/BIM

飞渡科技数字孪生重庆管理平台&#xff0c;以实景三维平台为支撑&#xff0c;以城市数据库对接为核心&#xff0c;利用数字孪生技术&#xff0c;结合云计算、物联网IOT等技术&#xff0c;对接城市规划、智能交通、和公共安全等系统。 利用平台强大的国产自研渲染引擎&#xff0…

你的职业规划就是面向贫穷的规划

如果你觉得作者的文章还有点用,请记得点赞 + 关注 说一个扎心的事实,就是我们绝大多数人的职业规划基本上都是错误的,都是面向贫穷的规划。 因为绝大多数人的职业规划都是打工人的职业规划,这种规划除了很少部分人最终能成为企业高管,实现层级跃迁外,绝大多数人在大多数…

AI大模型探索之路-实战篇:智能化IT领域搜索引擎之知乎网站数据获取(初步实践)

系列篇章&#x1f4a5; No.文章1AI大模型探索之路-实战篇&#xff1a;智能化IT领域搜索引擎的构建与初步实践2AI大模型探索之路-实战篇&#xff1a;智能化IT领域搜索引擎之GLM-4大模型技术的实践探索3AI大模型探索之路-实战篇&#xff1a;智能化IT领域搜索引擎之知乎网站数据获…

Spring Boot:Java 应用开发高效之道

Spring Boot 是一种革命性的框架&#xff0c;旨在简化 Java 应用的创建和部署过程。通过自动化配置和简化项目搭建流程&#xff0c;Spring Boot 大大加速了开发周期&#xff0c;让 Java 应用开发变得更加高效和便捷。 核心优势&#xff1a; 快速启动和简化配置&#xff1a;Spr…

wordpress轻量免费主题

WordPress建站公司 适合提供WordPress建站服务的公司或个体(个人)工作室使用的WordPress建站公司主题模板。 https://www.jianzhanpress.com/?p545 首屏大图红色简洁wordpress主题 首屏大图红色简洁wordpress主题&#xff0c;非常地高端大气上档次&#xff0c;可用于多个行…

感受光子芯片中试线,如何点亮未来计算与通信的革命之路(2024青岛智能装备与通信技术展)

光子芯片中试线&#xff1a;点亮未来计算与通信的革命之路 在新一代信息技术的浪潮中&#xff0c;光子芯片以其低能耗、高速度的特点备受瞩目。首条光子芯片中试线的建立&#xff0c;标志着我国在光电子领域的重大突破&#xff0c;同时也为即将到来的量子计算时代奠定了坚实基…

机器学习python实践——数据“相关性“的一些补充性个人思考

在上一篇“数据白化”的文章中&#xff0c;说到了数据“相关性”的概念&#xff0c;但是在统计学中&#xff0c;不仅存在“相关性”还存在“独立性”等等&#xff0c;所以&#xff0c;本文主要对数据“相关性”进行一些补充。当然&#xff0c;如果这篇文章还能入得了各位“看官…