使用Texture2DArray构建GPU Particle

使用Texture2DArray构建GPU Particle

【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。

更多精彩内容请关注:lab.uwa4d.com


一、前言

现代图形API支持“纹理数组”,它是具有相同大小和格式纹理的数组。它们被着色器视为单个资源,对它们进行采样需要一个额外的坐标,指示要从中采样的数组元素。

通常,纹理数组可用作纹理图集的替代,或者在对象使用一组相同大小纹理的其他情况,例如:同样几何形状的不同角色,各自有不同的贴图,或者该物体有多个不同属性的纹理贴图例如漫反射颜色、法线贴图、高光密度贴图等。

在上述情况下,我们需要使用多个纹理的时候,需要在绘制命令之前将所有的所需纹理绑定完毕。当我们还需要更新纹理对象本身时,每一次材质切换(类似glBindTexture)都会对性能产生一些影响。纹理数组可以将这些尺寸格式相同的纹理合并到一个集合,直接减少材质切换的次数,甚至可以降低DrawCall的数量,从而提高性能。Unity纹理数组就提供了这样的功能。

而对于具有不同贴图的粒子系统,就可以使用Texture2DArray来实现。今天介绍的这个开源库项目就提供了一个解决方案。

开源库链接:https://lab.uwa4d.com/lab/5b66392bd7f10a201ffa7e14


二、效果展示






三、使用方式、代码分析与简单原理

该项目提供了一些Demo以供学习。

1、载入相应的贴图,要求格式尺寸相同。并赋予TextureArray中的Textures数组。

请输入图片描述

TextureArray类用于新建Texture2DArray并读入像素数据。目前在Unity纹理数组中没有导入管道,必须在运行时或编辑器脚本中从代码创建。使用Graphics.CopyTexture对于将像素数据从常规2D纹理快速复制到纹理数组。将该纹理数组传递给GPUParticleSystem材质(Butterfly着色器)。

void Start () {
//构造Texture2DArray
  array = new Texture2DArray(width, height, count, TextureFormat.RGB24, false);
  array.Apply();
  material.SetTexture("_TextureArray", array);
  material.SetFloat("_Depth", count);
  Load(textures);
}

void Load (List<Texture2D> textures) {
  var candidates = textures.FindAll(tex => tex.format == TextureFormat.RGB24).ToList();
  int cn = candidates.Count;
  for(int i = 0; i < count; i++) {
    //读取图片像素信息
    Graphics.CopyTexture(textures[i % cn], 0, 0, array, current, 0);
    current = (current + 1) % count;
  }
  array.Apply();
  material.SetTexture("_TextureArray", array);
}

2、FboPingpong类继承System.IDisposable,便于实现资源释放,该类包含两张RenderTexture,用于实现双缓冲绘制。

GPUParticleProp类继承System.IDisposable,该类包含一个关键词和一个FboPingpong类,根据关键词向对应材质、着色器传递FboPingpong中的RT。

3、GPUParticleUpdaterController脚本用于控制效果切换,当演示效果时长达到规定时长或者按下”N“键,停止当前协程,新建协程调用Step(),激活下一个效果的GPUParticleUpdaterGroup中每一个GPUParticleUpdater。

IEnumerator Repeater () {
  yield return 0;
  while(true) {
    Step ();
    yield return new WaitForSeconds(current.Duration);
  }
}

void Step () {
  if(current != null) current.Deactivate();
  current = groups[index % groups.Count];
  current.Activate();
  index++;
}

4、GPUParticleUpdaterGroup中包含多个GPUParticleUpdater用于实现对应效果,GPUParticleUpdater类及其子类,接收GPUParticleProp类中的FboPingpong类的RT,将实现效果渲染到RT上。

例如:ApplyRotation子类接收RT。

public override void Render (GPUParticleSystem system) {
  var velocity = system.GetProp("_Velocity");
  var rotation = system.GetProp("_Rotation");
  if(velocity != null && rotation != null) {
    material.SetTexture(velocity.Key, velocity.FBO.ReadTex);
    material.SetTexture(rotation.Key, rotation.FBO.ReadTex);
    Blit(rotation.FBO, material);
    }
}

渲染RT:

protected void Blit (FboPingpong fbo, Material mat, int pass = -1) {
  Graphics.Blit(null, fbo.WriteTex, mat, pass);
  fbo.Swap();
}

5、将上述得到的RT传递给GPUParticleSystem中的材质GPUParticleSystem材质(Butterfly着色器)。

GPUParticleSystem会在项目启动时生成一个全部是点的Mesh。

请输入图片描述

相关代码:

Mesh Build (int count = 10000) {
  var mesh = new Mesh();
  int dcount = count * count;
  var vertices = new Vector3[dcount];
  var uv = new Vector2[dcount];
  var indices = new int[dcount];
  for(int i = 0; i < dcount; i++) {
    int k = i;
    float tx = (1f * (k % count)) / count;
    float ty = (1f * (k / count)) / count;
    vertices[i] = Random.insideUnitSphere;
    uv[i] = new Vector2(tx, ty);
    indices[i] = i;
  }
  mesh.vertices = vertices;
  mesh.uv = uv;
  mesh.SetIndices(indices, MeshTopology.Points, 0);
  return mesh;
}

在Update的时候,通过Butterfly着色器,根据点的位置绘制面片(Geometry Shader),渲染得到一个网格Mesh,获得效果。

请输入图片描述

GPUParticleSystem中绘制Mesh相关代码:

void Update () {
  updaters.ForEach(updater => {
    if(updater != null && updater.isActiveAndEnabled) {
      updater.Render(this);
      }
   });
  props.ForEach(prop => {
    material.SetTexture(prop.Key, prop.FBO.ReadTex);
  });
  Graphics.DrawMesh(mesh, transform.localToWorldMatrix, material, 0);
}

Butterfly Shader中相关代码:

#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
[maxvertexcount(8)]
void geom (point v2g IN[1], inout TriangleStream<g2f> triStream) {
  float halfS = 0.5f * _Size;
  float3 right = rotate_vector(float3(1, 0, 0), IN[0].rot) * halfS;
  float3 up = rotate_vector(float3(0, 1, 0), IN[0].rot) * halfS;
  float4 v[6];
  //绘制一个面片
  v[0] = float4(IN[0].pos + halfS * right - halfS * up, 1.0f);
  v[1] = float4(IN[0].pos + halfS * right + halfS * up, 1.0f);
  v[2] = float4(IN[0].pos                 - halfS * up, 1.0f);
  v[3] = float4(IN[0].pos                 + halfS * up, 1.0f);
  v[4] = float4(IN[0].pos - halfS * right - halfS * up, 1.0f);
  v[5] = float4(IN[0].pos - halfS * right + halfS * up, 1.0f);

想要拓展更多的效果可以构建GPUParticleUpdater的子类及对应材质和着色器,组成一个新的GPUParticleUpdaterGroup,并添加到GPUParticleUpdaterController类的groups中。

请输入图片描述


四、性能测试

由于所使用的着色器使用了Geometry Shader,故在安卓端受到很大的使用限制。但可以在Editor里的Frame Debug中查看DrawCall数量:

请输入图片描述

可以看出,借助Texture2DArray可以将DrawCall 降为1,这样可以大幅提升CPU端的渲染性能。


五、注意事项

当使用高版本的Unity打开项目,进行自动升级操作后:需要将Butterfly Shader第119行中的:

float4x4 vp = UnityObjectToClipPos(unity_WorldToObject);

改为:

float4x4 vp = UNITY_MATRIX_MVP;

今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......

请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。