XGundam05 发表于 2021-12-25 18:56

Unity渲染绘制接口(一)渲染组件

一、前言

通常项目中能够看到的对象,都是通过内置资源(Cube、Plane)或者外部导入资源(FBX、PNG)来支持的。本篇将会梳理Unity提交渲染数据的方式。
Unity封装了一些渲染组件,方便使用者导入数据即可渲染,同时还给出了底层渲染的接口:GL和Graphics,对渲染更精细处理。与渲染绘制息息相关的一个问题是:我们需要在什么时候进行渲染?在内置管线中可使用CommanderBuffer定义渲染节点,而SRP提供了更易用的接口,两者概念比较接近。
因此渲染绘制需要梳理的问题就明确了:在什么时刻用什么方式进行渲染?本篇会先梳理有哪些渲染方式,然后对渲染流程做分析。
二、渲染组件

渲染组件是项目中最常见到的渲染方式,其定制化了很多数据处理流程,方便易用,程序通常不需要关注内部实现。但项目也会出现一些特殊需求或者性能优化的情况,此时就需要分析其处理过程了。
2.1 Mesh & Mesh Filter & Mesh Renderer




Mesh Renderer vs Skinned Mesh Renderer

Mesh是Unity提供网格数据类型,Mesh Renderer对Mesh数据在程序中进行渲染,而Mesh Filter起到桥梁的作用,将Mesh传递到Mesh Renderer。官方解释由于历史原因,Mesh Renderer无法像蒙皮网格一样直接使用Mesh,因此才有了Mesh Filter。
Mesh和Mesh Renderer在C++层处理,无法知晓具体处理方式,Mesh Renderer根据Mesh进行渲染,因此若需要自定义网格,就需要关注Mesh。Mesh关键属性如下:
// 顶点属性
public Vector3[] vertices;
public Vector3[] normals;
public Vector4[] tangents;
public Vector2[] uv;
public Color[] colors;
// 三角形索引
public int[] triangles;
其结构与OpenGL中的概念类似,triangles存储顶点数据的索引,每三个索引组成一个三角形(顶点以顺时针顺序排列)。以绘制一个矩形为例,顶点数组中存储4个顶点数据,triangles存储6个顶点索引(3*2)。


Mesh mesh = new Mesh();
mesh.vertices = new Vector3 {
    new Vector3 (-1, -1, 0),
    new Vector3 (-1, 1, 0),
    new Vector3 (1, 1, 0),
    new Vector3 (1, -1, 0),
};
mesh.triangles = new int { 0, 1, 2, 2, 3, 0 };
GetComponent<MeshFilter>().mesh = mesh;
GetComponent<MeshRenderer>().material = mat;
以自定义球体网格为例,演示网格构建的过程。根据网格划分精度,使用球坐标生成顶点数据,再依次生成三角形数据。以小球标记生成的顶点,运行时动态修改显示的三角形。(通常顶点动画会使用Shader处理,而不是修改Mesh,此处只是为了演示数据结构,Demo见文底RanderAPI场景)

动态显示球体网格
https://www.zhihu.com/video/1454186573637685248
2.2 UI - Graphic

Unity中大多UI渲染组件都继承于Graphic,常见的如Image、Text。2019.3.11f版本中Graphic关键定义如下


public abstract class Graphic : UIBehaviour, ICanvasElement
{
    protected Material m_Material;// 材质
    private RectTransform m_RectTransform;// 绘制区域
    protected Mesh m_CachedMesh;// 顶点属性
    public virtual Texture mainTexture;// 主纹理

    public virtual void SetAllDirty()
    {
      …
      SetLayoutDirty();
      …
      SetMaterialDirty();
      …
      SetVerticesDirty();
    }

    protected override void OnEnable()
    {
      base.OnEnable();
      CacheCanvas();
      GraphicRegistry.RegisterGraphicForCanvas(canvas, this);

      if (s_WhiteTexture == null)
            s_WhiteTexture = Texture2D.whiteTexture;

      SetAllDirty();
    }


    protected virtual void OnPopulateMesh(Mesh m)
    {
      OnPopulateMesh(s_VertexHelper);
      s_VertexHelper.FillMesh(m);
    }

    protected virtual void OnPopulateMesh(VertexHelper vh)
    {
      var r = GetPixelAdjustedRect();
      var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);

      Color32 color32 = color;
      vh.Clear();
      vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f));
      vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f));
      vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f));
      vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f));

      vh.AddTriangle(0, 1, 2);
      vh.AddTriangle(2, 3, 0);
    }
}
UI更新通过设置脏节点来处理,UI更新会使得Canvas Rebuild。SetAllDirty方法中可以看出,影响UI重构的因素:布局位置、材质、顶点数据。
在组件启动时通过GraphicRegistry记录Graphic数据,将组件设置为脏节点,通知Canvas刷新UI。
OnPopulateMesh中提交顶点数据,可以看到Unity默认为UI创建了对应RectTransform的矩形区域,若需要自定义UI区域也可以对此方法重载。
以玫瑰曲线为例,动态修改顶点数据以达到类似Mask的效果。(Demo见文底RanderAPI场景)

玫瑰曲线
https://www.zhihu.com/video/1454278221994483712

2.3 UI - Text

UGUI处理文本显示使用Text组件,继承Graphic。对于字体图集的处理,通过key值记录文本在图集中uv坐标。在渲染文本时,只需要找到字体UV绘制到组件区域。
public class Text : MaskableGraphic, ILayoutElement
{
    public Font font{get; set;}

    protected override void OnPopulateMesh(VertexHelper toFill)
    {
      // 生成文本渲染数据
      Vector2 size = base.rectTransform.rect.size;
      TextGenerationSettings generationSettings = GetGenerationSettings(size);
      cachedTextGenerator.PopulateWithErrors(text, generationSettings, base.gameObject);
      // 传入顶点数据
      IList<UIVertex> verts = cachedTextGenerator.verts;
      ...
      toFill.Clear();
      float num = 1f / pixelsPerUnit;// 一个像素对应的UI大小
      ...
      // 添加三角形(矩形)
      for (int i = 0; i < count; i++)
      {
            int num2 = i & 3;
            m_TempVerts = verts;
            m_TempVerts.position *= num;
            m_TempVerts.position.x += vector.x;
            m_TempVerts.position.y += vector.y;
            if (num2 == 3)
            {
                toFill.AddUIVertexQuad(m_TempVerts);
            }
      }
    }
}
UGUI文本的处理方式很容易理解,但由于每个字符都对应一部分图片,在处理中文时其包体很容易膨胀,程序资源的加载与卸载也需要严格注意。以固定大小纹理处理字体,问题在于较大区域采样时,显示模糊。TextMeshPro使用矢量图处理文本,较好解决了渲染模糊问题,但使用相对繁琐。



Text网格

2.4 Skinned Mesh Renderer

对于角色运动需求,相对静态的Mesh Renderer就难以处理了,仿照生物的结构产生了蒙皮骨骼动画的处理方式,Unity提供了SkinnedMeshRenderer来处理这一问题。动画关键帧存储骨骼数据,关键帧之间通过插值的方式计算骨骼位置;Mesh绑定到骨骼,根据骨骼位置动态调整顶点数据。(骨骼是一组具有父子关系的节点,可以理解为Unity的Transform)



来自Unity官方第三人称demo

public class SkinnedMeshRenderer : Renderer
{
    public Transform rootBone{get; set;}// 骨骼根节点
    public Transform[] bones{get; set;} // 所有骨骼数据
    public Mesh sharedMesh{get; set;}   
    public Bounds localBounds{get; set;}// 包围盒
    public void BakeMesh(Mesh mesh){...}// 烘焙当前角色蒙皮网格
}
参考


[*]Unity - Manual: Graphics
[*]Unity - Scripting API: Mesh
[*]Coder1024:球坐标与直角坐标间的转变
[*]启思:浅谈骨骼动画技术原理(一):基本介绍
[*]Shader实验室:3D骨骼动画(一):原理
页: [1]
查看完整版本: Unity渲染绘制接口(一)渲染组件