Unity Shader(一) Lowpoly动态低多边形 (QQ登录界面低边动画)

前言

在逛论坛的时候偶然发现有人在问动态低多边形(Lowpoly)是如何实现的,因为经常编写UGUI拓展对顶点操作较为熟悉的我立马就想到利用继承UnityEngine.Graphic,重写OnPopulateMesh方法绘制顶点、赋值颜色,在Update方法中计算顶点位置使得顶点在进行连续且不无断点的路径上产生位移即可,当然这只是初步的设想,这种方式能实现动态低边效果,但是不同的三角面展现的高光效果在UI也不是一件简单的事情,所以我们摒弃2DUGUI的方式,使用网格编程和Shader(着色器)来实现这一效果。

本文合适对向量运算和Shader有一定了解的人员,当然你也可以直接使用成果。

实现效果

网格使用代码生成,使得我们有更多可配置的余地

  • 变种一

1

  • 变种二

2

  • Inspecetor

这里写图片描述

主要内容

  • 绘制网格
  • 网格持久化
  • 编写Shader
  • 网格动态化

详细设计

Unity中网格要可见还需要两个额外的好搭档Material和Shader,所以我们先创建好三个必备文件Lowpoly.cs(C#代码绘制网格)、Lowpoly.material(材质球)和LowpolyShader(着色器,用于给材质球着色)。

这里写图片描述

  • 先补一个最终想要实现的大致效果

嗯,请脑补掉企鹅大厂的Logo : )

这里写图片描述

绘制网格

首先通过观察图片我们需要得知大致的绘制思路,大概如下几点:

  • 绘制一个NxM的网格
  • 改变网格顶点的位置,实现网格错乱
  • 重点:每个三角形颜色一致,并没有顶点到顶点的颜色过度
  • 要计算法线,来实现不同角度的反射不同程度的来亮度
  • 将改变网格的算法移动到Update函数中,实现动画效果
绘制原理

我们就根据上述已经总结好的几点思路来逐步讲解原理

  • 上一个NxM的效果图

5x5

  • 绘制NxM的网格

    1. 在Unity中网格存储在MeshFilter组件的mesh属性里,所以我们在将绘制好的网格存入MeshFilter.mesh属性即可。
    2. 要绘制mesh网格需要向mesh网格中写入顶点位置,贴图uv值(如果你不贴贴图的话也可以不赋值),三角面对应顶点的序号,顶点法线(如果不需要模型的细节也不可以不赋值法线,稍后我们再谈法线的问题),顶点切线(用于处理细节),这里暂不考虑tangent。
    3. 着色器的会为每一个顶点进行光照计算并并给顶点赋值颜色,如果A点到B点是三角面上的一条边,那么A点到B点的中间的颜色为A点的颜色到B点颜色的过渡色。但是因为我们要实现的效果中,三角面的颜色不存在过渡色,也就意味着着色器处理过后的三个顶点的颜色值一样,也就要求三个顶点的顶点法线是相同的,所以在处理网格法线时,我们需要将构成三角面的网格的三个顶点法线设置为同一个向量,同时这样也就意味着看似在一起的顶点也不能够共用,共用同一个顶点就会导致相邻两个三角面的法向量相同,从而导致所有顶点法向量相同导致无法曾现层次感。所以我们要为每一个三角面创建三个顶点。又因为每个四边形都需要利用两个三角面来组合绘制,因此我们可计算出绘制动态低多边形所需要的三角面个数(TrianglesCount)顶点个数(VerticesCount)UV坐标(UVsCount)法线向量个数(NormalsCount):
TrianglesCount = N*M*2

VerticesCount = TrianglesCount*3

UVsCount = VerticesCount

NormalsCount = VerticesCount

有了以上大致的了解我们来计算顶点,顶点计算代码如下,为方便计算我们先计算一个矩形中右下角的三角形,再计算左上角的三角形

Mesh mesh = new Mesh();
mesh.name = "LowPoly";
size = new Vector3(1,1,0);
origin = new Vector3 (-size.x / 2.0f,-size.y/2.0f,0);
perX = size.x / XCount;
perY = size.y / YCount;

// 右下角三角面
for (int i = 0; i <= YCount; i++) 
{
    for (int j = 0; j <= XCount; j++)
    {
        if (j.Equals (XCount))
            continue;
        if (i.Equals (YCount))
            continue;
        m_vertices.Add (PosNormal (j,i));
        m_vertices.Add(PosNormal(j+1,i));
        m_vertices.Add(PosNormal(j+1,i+1));
        m_uvs.Add ( new Vector2( j * perX, i * perY));
        m_uvs.Add ( new Vector2( (j+1) * perX, i * perY));
        m_uvs.Add ( new Vector2( (j+1) * perX, (i+1) * perY));
    }
}
// 左下角三角面
for (int i = 0; i <= YCount; i++) 
{
    for (int j = 0; j <= XCount; j++)
    {
        if (j.Equals (XCount))
        continue;
        if (i.Equals (YCount))
            continue;
        m_vertices.Add (PosNormal (j,i));
        m_vertices.Add(PosNormal(j+1,i+1));
        m_vertices.Add(PosNormal(j,i+1));
        m_uvs.Add ( new Vector2( j * perX, i * perY));
        m_uvs.Add ( new Vector2( (j+1) * perX, (i+1) * perY));
        m_uvs.Add ( new Vector2( (j) * perX, (i+1) * perY));
    }
}

以上干货部分没有看懂的同学也不着急,我从外网找到一篇很有价值的网格入门教程,我会抽空翻译出来,详细原理看那篇文章,链接我也会附在这里。原文飞机票:http://catlikecoding.com/unity/tutorials/procedural-grid/

  • 赋值三角面序号

赋值三角面序号的过程就是告诉着色器每个三角面的三个顶点对应传入顶点数组中的哪个顶点,所以每个三角面都要指定三个顶点坐标位置。

这里需要注意顶点的渲染顺序,序号按逆时针顺序传入相机正面可见,顺时针传入相机逆面可见。

这里写图片描述

// 指定右下角三角面序号
m_triangles = new int[XCount * YCount * 6];
for (int i = 0 ,count = 0 ,total = 0 ; i < m_triangles.Length / 2 ; count ++ )
{
    if (((count + 1) % (XCount + 1)).Equals (0))
        continue;
    m_triangles[i] = total + 1;
    m_triangles[i + 1] = total;
    m_triangles[i + 2] = total + 2; 
    i += 3;
    total += 3;
}
// 指定左上角的三角面序号
int startIndex = m_vertices.Count / 2;
for (int i = m_triangles.Length / 2, count = 0 ,total = m_vertices.Count / 2; i < m_triangles.Length; count++)
{
    if (((count + 1) % (XCount + 1)).Equals (0))
        continue;
    m_triangles [i] = total + 2;
    m_triangles [i + 1] = total + 1;
    m_triangles [i + 2] = total ;
    i += 3;
    total += 3;
}
  • 计算法向量

    1. 严格上来说,一个顶点不可能有法线。但当使用Phong或Gouraud着色过程进行光照计算时,点法线提供了模拟光滑表面的一种方式。想象一个人体的多边形网格模型:这个模型只是一些多边形。但是这个网格模型能模拟一个人体。如果一个多边形里面的所有像素都使用相同的颜色着色,那么这个多边形看起来会非常平坦;但是通过使用点法线,我们能够对三角形的不同顶点应用不同的光照,这样就能够产生比较光滑的显示效果。该段内容参考至:生成点法线(Generating Vertex Normals) 又因为我们的效果中要求三个顶点的法向量相同,所以我们就没必要去计算法向量,直接用面法向量代替即可。如果你纠结顶点法向量的计算方法,点击使用这张飞机票 关于点法线向量的计算

    2. 面法向量如何计算呢?组成一个面的两个向量进行叉积就是法向量,不明白的同学也可以看看关于点法线向量的计算

按照下图两个向量的叉乘即可求出三角面的面法向量

计算面法向量参考图:

面法向量

m_normals = new Vector3[m_vertices.Count];
for (int i = 0; i < m_normals.Length; i+=3 )
{
    // 计算三角面上的两条向量
    Vector3 v1 = m_vertices[i + 1] - m_vertices[i];
    Vector3 v2 = m_vertices[i + 2] - m_vertices[i];
    // 叉乘获取面法向量
    Vector3 argNormal = -Vector3.Cross(v1,v2).normalized;
    // 赋值这三个顶点的法向量
    m_normals[i] = argNormal;
    m_normals[i + 1] = argNormal;
    m_normals[i + 2] = argNormal;
}

网格持久化

相信大家也已经发现,编写好的网格只能在程序运行的时候才能看到,那么我们应该如何把它保存下来呢,这里需要用到编辑器拓展方法,在编辑器拓展方法中调用我们上面已经编写好的网格生成算法即可把网格记录到MeshFilter组件中。

using UnityEditor;
using UnityEngine;

// 网格持久化
public class MeshPresistence
{
    // 使用此特性在工具栏生成按钮以调用改方法
    [MenuItem("Tools/Mesh/Presistence")]
    public static void Presistence()
    {
        // 获取当前选中的游戏物体
        GameObject selectedGo = Selection.activeGameObject;
        MeshFilter meshFilter = selectedGo.GetComponent<MeshFilter>();
        // 调用网格生成算法并记录持久化
        meshFilter.mesh = selectedGo.GetComponent<LowPoly>().GenerateLowPoly();
    }
}

然后再工具栏里找到我们创建的按钮,点击!然后双击游戏物体MeshFilter组件上Mesh就可以在Unity的右下角明确看到当前创建的网格的顶点个数和三角面个数和对模型的预览。

模型的预览

编写Shader

Shader的书写方法这里不再讲解,如果你不会编写Shader那就直接赋值一下Shader代码,并在unity中创建一个着色器附到材质球查看效果。

这里写图片描述这里写图片描述
这里写图片描述这里写图片描述

本文Shader的原理和边缘光的Shader类似,计算Diffuse漫反射光凸显模型轮廓,计算边缘光使得模型有较亮或叫暗的面,本shader是片面着色器,使用表面着色器应该会更加简单,后续我会附上表面着色器和固定渲染着色器,供大家参考学习。

Shader "Custom/LowpolyShader" 
{
    Properties 
    {       
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        // 高亮/边缘光颜色
        _SpecularColor ("Specular Color",Color) = (0.1,0.1,1,1)
        // 高亮/边缘光强度
        _SpecualrStrength ("Specular Strength",float) = 1.0 
    }

    SubShader 
    {
        Tags { "RenderType"="Opaque"  }

        pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _SpecularColor;
            float _SpecualrStrength;

            struct Input 
            {
                float4 position : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Out
            {
                float4 pos : SV_POSITION;
                float2 uv : Texcoord0;
                float3 normal : NORMAL;
            };

            Out vert( Input i )
            {
                Out o;
                // 转化屏幕坐标系位置
                o.pos = mul(UNITY_MATRIX_MVP,i.position);
                // 将本地坐标系法向量转化为世界坐标系方向量
                o.normal = mul(float4(i.normal,1),_World2Object).xyz;
                o.uv = i.uv;
                return o;
            }

            fixed4 frag( Out o ) : COLOR
            {
                // 法向量标准化
                float3 normal = normalize(o.normal);
                // 获取平行光源方向并标准化
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 获取贴图纹理 这步可有可无,取决于你是否贴贴图
                float3 texColor = tex2D(_MainTex,o.uv);
                // 计算漫反射
                fixed3 diffuseColor =  texColor * _Color * max(0,dot( normal,lightDir )) * _LightColor0.rgb;
                // 计算边缘光
                float spe = 1 - max(0,dot(normal,lightDir)) ;
                fixed3 speColor= _SpecularColor.rgb * pow(spe,_SpecualrStrength) ;
                // 混合输出
                return fixed4(diffuseColor + speColor,1.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

网格动态化

网格动态化原理:获取网格中心点的顶点(非边缘点)在update函数中赋值新的顶点位置并重新绘制网格,即可实现动态化,只修改中心顶点,可避免模型变形。

中心顶点

var indexVertices = new List<Vector3>();
timer += Time.deltaTime * Speed;
for (int i = 0; i <= YCount; i++)
{           
    for (int j = 0; j <= XCount; j++)
    {
        indexVertices.Add(PosNormal(j, i));
        if (i.Equals(YCount) || j.Equals(XCount) || i.Equals(0) || j.Equals(0))
            continue;                
        // 计算xyz的偏移值,Z轴的偏移值决定了顶点的法线向量和三角面的颜色亮度
        float offsetX = Mathf.Cos(timer) / 15;
        float offsetY = Mathf.Sin(timer) / 20;
        float offsetZ = Mathf.Sin(timer) * 10;
        // 乘以随机权重值,每个顶点的位移权重值不同,再与第一次绘制的顶点位置相加,避免直接操作顶点导致顶点位置跑偏
        Vector3 pos = new Vector3(offsetX,offsetY,offsetZ) * randomWeight[(XCount+1) * i + j] + originRandom[ (XCount+1) * i + j ];
        indexVertices[indexVertices.Count - 1] = pos;
    }
}
// 将新计算的顶点用于重新绘制网格
TransformLowpoly(indexVertices);
  • 场景中的最终效果

这里写图片描述

后续拓展

1.通过上述我们已经实现动态低多边形的效果,但是和QQ登录界面的效果还是有一定的差距,主要差在金属的反光效果和动态流光,后续我会考虑升级该效果,加入动态聚光灯来模拟实现。
2.后续我也继续更新一些其他的网格编程结合Shader的文章,比入利用网格shader实现积雪效果,实现海浪效果。嗯嗯,期待吧……因为我还要更新UGUI组件。
3.该篇博客的脚本和shader需要的话在评论下面留下邮箱吧,小内容不想上传github..

UGUI组件系列

Unity框架解读系列

分享地址(置顶目录包含所有组件的最新下载地址)

猜你喜欢

转载自blog.csdn.net/qq_29579137/article/details/77017043