简介

接着上一篇基于几何着色器的顶点渲染文章。
由于需求来自一个手游项目,在初步实现效果请主程看过后,被告知几何着色器在许多移动端不被支持,需要使用别的方法实现。
为此,考虑使用脚本,调用Mesh.SetIndices直接更改模型为线框模型及顶点模型来实现效果。
本篇将从Unity中的Mesh开始介绍,最终实现如下图的效果。

alt

实现

首先介绍Mesh中核心的两个属性,vertices和triangles数组。
vertices是一个Vector3数组,其中存放了Mesh中所有顶点的位置,其长度为Mesh的顶点数。可以通过for循环遍历vertices数组来获取顶点信息。
triangles数组是一个int数组,刚接触mesh的同学可能比较奇怪,如何通过int数组来存储三角面信息。所以许多TA会使用第三方封装好的一些库来处理mesh。这里triangles从第一个元素开始,每3个元素存储一个三角面,每个元素代表该顶点在vertices数组中的索引。即triangles数组并不存储顶点的详细信息,仅存储该三角面是由vertices中的哪几个顶点组成的。所以triangles数组的顺序极为重要。其长度为mesh中三角面数的三倍。
这里需要注意,triangles数组的长度往往大于vertices数组,因一个顶点可被最多六个三角面同时占用。可以通过Debug的方式将Unity自带的Sphere网格的vertices数组长度和triangles数组长度打印出来看看。

Debug.Log("Vertices:" + _mesh.vertices.Length);
Debug.Log("Triangles:" + _mesh.triangles.Length);



其结果为: alt



在理解了vertices和triangles后,就可以开始通过原网格构建新的线框网格和顶点网格了。
我们需要先把原mesh中的信息按照我们想要的顺序存储。

for (int i = 0; i < _mesh.triangles.Length; i += 3)
{
        vertexList.Add(_mesh.vertices[_mesh.triangles[i]]);
        vertexList.Add(_mesh.vertices[_mesh.triangles[i + 1]]);
        vertexList.Add(_mesh.vertices[_mesh.triangles[i + 2]]);
        
        normalList.Add(_mesh.normals[_mesh.triangles[i]]);
        normalList.Add(_mesh.normals[_mesh.triangles[i + 1]]);
        normalList.Add(_mesh.normals[_mesh.triangles[i + 2]]);
            
        uvList.Add(_mesh.uv[_mesh.triangles[i]]);
        uvList.Add(_mesh.uv[_mesh.triangles[i + 1]]);
        uvList.Add(_mesh.uv[_mesh.triangles[i + 2]]);
}

这一步是将所有顶点、顶点法线及uv信息按triangles数组的顺序重新读取并保存。为了让顶点信息按照可控顺序排布,这一步会使vertices中的信息出现冗余。
alt

在得到足够的信息后,就可以构建新的网格了。
先来构建线框网格。要构建线框网格,必须先提供线框数组lines,lines的结构类似triangles,从第一个元素开始,每两个元素表示一条线,其中每个元素为这条线的端点在vertices中的索引。所以lines的顺序也极为重要,一但出错效果会完全错误。lines的长度是mesh中线条数目的2倍,进一步换算可知,其长度为三角面数目的6倍,也为triangles数组长度的2倍。由于前面已经按triangles数组的顺序对顶点信息进行了排序,所以这里我们可以直接简单构建lines数组。

for (int i = 0; i <  _mesh.triangles.Length; i += 3)
{
        int anchor = i * 2;

        lines[anchor++] = i;
        lines[anchor++] = i + 1;

        lines[anchor++] = i + 1;
        lines[anchor++] = i + 2;

        lines[anchor++] = i + 2;
        lines[anchor++] = i;
 }

上面这段代码以三角形为单位来构建线条,每次构建三条。
在构建好线框网格后,构建顶点就显得相对简单,只要将vertices按续排成int数组即可。当然这样做会有大量顶点冗余,目前暂时不考虑优化问题。

for (int i = 0; i < _mesh.triangles.Length; i++)
{
        index[i] = i;
}

现在得到了顶点网格和线框网格后,只要将这两个网格设置为原网格的submesh覆盖原网格即可。

_mesh.subMeshCount = 2;
_mesh.SetIndices(lines, MeshTopology.Lines, 0);
_mesh.SetIndices(index, MeshTopology.Points, 1);

利用submesh的机制,可通过给物体多个material来实现不同效果,十分便利。

总结

使用更改Mesh的方法可以在不使用几何着色器的情况下实现线框和顶点渲染,保证在移动平台的运行。但是这样做有一个十分明显的问题是,无法控制线框的粗细和顶点的大小,其大小基本固定在2, 3个像素点。为了弥补这一问题,可以通过shader为其赋HDR色彩,并在后处理中开启bloom来增强其效果。

源码

这里仅给出生成新网格的C#脚本,shader的着色参考了上一篇几何着色器渲染线框的着色,音频分析脚本不在本篇中给出。

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using UnityEngine;

public class CreateFrame : MonoBehaviour
{
    private Mesh _mesh;
    void Start()
    {
        _mesh = GetComponent<MeshFilter>().mesh;

        List<Vector3> vertexList = new List<Vector3>();
        List<Vector3> normalList = new List<Vector3>();
        List<Vector3> uvList = new List<Vector3>();
        
        for (int i = 0; i < _mesh.triangles.Length; i += 3)
        {
            vertexList.Add(_mesh.vertices[_mesh.triangles[i]]);
            vertexList.Add(_mesh.vertices[_mesh.triangles[i + 1]]);
            vertexList.Add(_mesh.vertices[_mesh.triangles[i + 2]]);
            
            normalList.Add(_mesh.normals[_mesh.triangles[i]]);
            normalList.Add(_mesh.normals[_mesh.triangles[i + 1]]);
            normalList.Add(_mesh.normals[_mesh.triangles[i + 2]]);
            
            uvList.Add(_mesh.uv[_mesh.triangles[i]]);
            uvList.Add(_mesh.uv[_mesh.triangles[i + 1]]);
            uvList.Add(_mesh.uv[_mesh.triangles[i + 2]]);
        }
        
        int[] lines = new int[2 * _mesh.triangles.Length];
        int[] index = new int[_mesh.triangles.Length];

        for (int i = 0; i < _mesh.triangles.Length; i++)
        {
            index[i] = i;
        }

        for (int i = 0; i <  _mesh.triangles.Length; i += 3)
        {
            int anchor = i * 2;

            lines[anchor++] = i;
            lines[anchor++] = i + 1;

            lines[anchor++] = i + 1;
            lines[anchor++] = i + 2;

            lines[anchor++] = i + 2;
            lines[anchor++] = i;
        }

        _mesh.SetVertices(vertexList);
        _mesh.SetNormals(normalList.ToArray());
        _mesh.SetUVs(0, uvList);

        _mesh.subMeshCount = 2;
        _mesh.SetIndices(lines, MeshTopology.Lines, 0);
        _mesh.SetIndices(index, MeshTopology.Points, 1);

        _mesh.RecalculateBounds();
        _mesh.Optimize();
        _mesh.UploadMeshData(true);
    }
}