Unity中UI曲面化

7,027 阅读9分钟

在VR下面,曲面UI可以提升用户在场景中的沉浸感,获得更好的视觉体验

方案选择

  1. 做一套基于曲面的UI
    我们项目中基本只用到Image和Text两种,Image是比较好处理的,直接将Texture贴到一个曲面的mesh上就可以了,但是Text相对比较麻烦。我们无法简单的取到某一段文字的Texture,必须自己从字体文件里面裁剪每个文字的Texture,然后拼接到一个Texture,然后再将最终的纹理贴到一个曲面mesh上。

  2. 单独将平面UI渲染到一个纹理,然后贴到曲面mesh上
    这个方案在实现曲面化上是没有问题的,但是为了满足VR下面的立体效果,我们两只眼睛看到的东西是有一定差异的,通过这些差异才有立体感,如果是同一个曲面的UI Texture,UI上的一些立体效果会损失(比如UI向前浮动)。另外,如果使用这种方法,我们眼睛看到的UI和真实的UI有很大的差异,需要重新设计凝视输入。

  3. 让UI沿着曲面分布,且每个UI元素有曲面效果
    让UI沿着曲面分布是比较好实现的,只需要计算合理的位置和角度,让UI整体上呈现出曲面的效果。目前有很多VR的曲面效果都是采用这种简单的方法实现的,但是这种方案实现的曲面效果并不是很好,是一种假曲面化的效果。如下图所示,在上下边缘可以看到很明显的直线。

    UI元素沿着曲面分布
    UI元素沿着曲面分布

    所以在沿着曲面分布的前提下,如果每个”UI元素都有曲面效果“,那么整体上才会看出曲面效果。如下图所示
    UI元素都有曲面效果
    UI元素都有曲面效果

结合实现难度和效果,我们选择了方案 3

曲面化中的数学原理

无论是让UI沿着曲面分布,还是实现每个UI自身的曲面效果,实质上都是做同一种数学运算——计算平面上的点映射到曲面上的坐标。
由于我们曲面化是一个圆柱面,圆柱轴心线与Y轴平行,变化前后Y轴坐标是一样的,下面是原理:

掘金不支持数学公式
掘金不支持数学公式

几何示意图
几何示意图

其中的关键点是变换前后的长度对应关系弧长与半径的比是角度。上面给出的过程是一种特殊情况,实际过程中会有些变化,比如圆心不在原点,但是都是可以通过以上的方法推导出来结果。

曲面化

让UI沿着曲面分布

对每个UI计算曲面化之后的坐标,上面已经给出了计算方法,需要注意的是,计算玩坐标之后还需要调整UI的角度,让UI的前方是圆心到变换之后坐标的方向。比如上面变换之后位于B点的UItransform.forward = transform.position.normalized
到了这一步,整体的UI就有了上面所说的假曲面化的效果。

每个UI元素的曲面效果

Unity提供了BaseMeshEffect对UI元素生成的mesh做一些修改来实现一些效果,不同Unity版本这个API有些差异,这里用到的是Unity5.3.4,主要是重写ModifyMesh(VertexHelper vh)方法。
ModifyMesh方法主要内容:

public override void ModifyMesh(VertexHelper vh)
{
        base.ModifyMesh(vh);
        if (!this.IsActive() || !bendEnable)
            return;

        /* 
        检查是否需要重新生成或修改顶点坐标,如果不需要,则使用已经缓存的顶点坐标 
        */

        if (cachedVertices == null || cachedTriangles == null || verticesDirty)
        {
            // 需要修改顶点,首先将Unity生成的顶点取出来
            List<UIVertex> originUIVertices = new List<UIVertex>();
            vh.GetUIVertexStream(originUIVertices);
            /*
            对顶点做一些变换,包括增加顶点以及重新计算顶点坐标,对于Image和Text有不同的处理方式
            */
        }

        // 如果材质改变,重新给定点着色
        if (materialDirty)
        {
            UpdateVertiecsColor(cachedVertices);
            materialDirty = false;
        }
        // 清除Unity生成的顶点,将我们重新计算的顶点设置到mesh上
        vh.Clear();
        vh.AddUIVertexStream(cachedVertices, cachedTriangles);
        // 根据生成顶点的类型也可以使用vh.AddUIVertexTriangleStream(cachedVertices)设置顶点
}

1. 检查顶点是否需要更新

当Unity发现UI需要更新的时候会调用ModifyMesh(VertexHelper vh),Unity自己触发UI更新的条件有尺寸改变和材质改变,我们也可以使用Graphic.SetAllDirty() Graphic.SetVerticesDirty() 触发。但是并非所有的情况下都需要重新计算顶点坐标,当我们计算出一个UI的曲面状态下的顶点之后,很少需要重新计算,我们只在UI尺寸改变的情况下才触发重新计算顶点,当然可以根据实际使用情况调整策略。顶点计算比较耗时,建议先判断在计算。

2. 取出顶点

取出来的顶点是一个UIVertex的列表,一般情况下,列表中每3个构成一个三角形,如果改变列表中元素的位置,会导致UI显示异常,所以最后输出给VertexHelper的顶点也是有顺序的。

3. 顶点计算

这一步中对于Text和Image有较大差异,主要原因在于一般的Text自己都有足够多且细分的三角形,只需要重新计算顶点的坐标就可以有很好的曲面效果,但是一般情况下,Image只有两个三角形(Sliced模式下Tiled模式会多一些,但是依然不够细分),四个顶点,对四个顶点重新计算之后依然是一个平面的效果,所以需要考虑给Image的mesh添加一些顶点,让Image上的三角形足够细分。

对于Text的处理
cachedVertices = new List<UIVertex>();
vh.GetUIVertexStream(cachedVertices);
BendMeshCylinder(cachedVertices);

其中的BendMeshCylinder(cachedVertices)函数就是将传入的顶点变换到圆柱曲面上,需要注意的是UIVertex里面的坐标是相对UI自身的局部坐标。处理过程不改变UIVertex列表的顺序,处理完的依然保持之前的三角形顺序,所以最后直接使用vh.AddUIVertexTriangleStream(cachedVertices)设置顶点。

对于Image的处理

一般情况下的Image只需要4个顶点就可以构成两个三角形,但是从VertexHelper里面取出来的顶点有6个,每三个构成一个三角形,重复使用了其中的两个顶点,如下图所示,第0,1,2和3,4,5分别构成一个三角形,0和5为同一个顶点,2和3为同一个顶点。


最初的想法是忽略Unity自己生成的顶点,直接在代码中根据原始顶点的规律生成一个足够细分的mesh,然后对所有顶点采用和Text里面相同的计算就可以有曲面效果,如下图所示,因为我们只需要圆柱曲面效果,所以在Y轴方向不需要细分,这样能大大减少三角形的数量。

这种方案一般情况下是够用的,但是当遇到Sliced或者Tiled模式的Image的时候就会有问题:当我们计算新增顶点坐标的时候,需要给顶点指定一个uv值,这个值将决定图片渲染在这一点的uv,普通图片的uv值是从0到1的均匀分布,所以直接根据新计算的坐标在整个Image上的位置就知道uv值,但是Sliced和Tiled的uv不是0到1的均匀分布,根据坐标是无法直接算出uv值的。比如下图是一个Sliced模式的mesh,编号为1的顶点Y轴上是整个Image高度的0.1,但是uv中v(Y轴)的值使0.3,如果我们忽略这些值,直接生成均匀分布的点,Sliced的特性就没有了

所以需要在保持Unity计算出来的顶点,然后再在这些顶点的基础上进行线性插值计算新增的顶点。代码如下:

List<UIVertex> originUIVertices = new List<UIVertex>();
vh.GetUIVertexStream(originUIVertices);
TrisToQuads(originUIVertices);
for (int i = 0; i < originUIVertices.Count; i += 4)
{
    CreateQuads(originUIVertices, i, cachedVertices, cachedTriangles);
}
BendMeshCylinder(cachedVertices);

首先在TrisToQuads(originUIVertices)里面是将每六个顶点构成的两个三角形合并为四个顶点构成的四边形,然后在CreateQuads(originUIVertices, i, cachedVertices, cachedTriangles)里面对每个四边形内部进行线性插值计算新增顶点,每个四边形内部点的uv都可以根据坐标在四边形内部的位置计算出来。如下图所示,红色点为新增顶点,当四边形在X轴方向足够细分,就不需要再添加顶点,通常情况下Sliced模式的图片边缘是足够细分的。


我们发现,四个顶点就足以描述两个三角形,但是需要知道两个三角形与四个顶点的对应关系,三角形更多的情况下可以通过顶点的复用使得顶点数量更少,比如上图中有36个三角形,但是只有28个顶点,我们需要缓存曲面变化之后的顶点,避免后面的重复计算,所以采用顶点复用可以让我们缓存的顶点数量大大的减少,但是我们需要缓存一个顶点与构成的三角形的对应关系的int列表,这个列表中每三个数描述一个三角形,数值对应着缓存的顶点列表的索引。顶点生成完成之后,和Text中一样对顶点的坐标进行曲面变换。最后我们设置顶点的时候需要告诉VertexHelper顶点与三角形的对应关系,vh.AddUIVertexStream(cachedVertices, cachedTriangles)

4. 改变顶点的颜色

如果改变UI的颜色属性,会触发MaterialDirty,我们可以通过Graphic.RegisterDirtyMaterialCallback监听这个改变,然后在ModifyMesh()改变顶点的颜色。另外不建议监听Graphic.RegisterDirtyVerticesCallback来确定是否需要重新计算顶点,因为改变顶点颜色,这个回调也会调用。


曲面化的原理如上,如果要真正运用起来,还需要配合一个Editor。需要注意的是,最好不要在曲面化状态调整UI的坐标和角度,不仅很难调整到想要的位置,而且会影响整体曲面化的效果。
文章里面主要聚焦圆柱面,如果是球面,主要要修改UI坐标的计算方法,和Image三角形细化方法,如果是其他更复杂的曲面,不太建议用这种方式处理,因为涉及到曲面的数据计算,效果也很难保证。