在逛论坛的时候偶然发现有人在问动态低多边形(Lowpoly)是如何实现的,因为经常编写UGUI拓展对顶点操作较为熟悉的我立马就想到利用继承UnityEngine.Graphic,重写OnPopulateMesh方法绘制顶点、赋值颜色,在Update方法中计算顶点位置使得顶点在进行连续且不无断点的路径上产生位移即可,当然这只是初步的设想,这种方式能实现动态低边效果,但是不同的三角面展现的高光效果在UI也不是一件简单的事情,所以我们摒弃2DUGUI的方式,使用网格编程和Shader(着色器)来实现这一效果。
本文合适对向量运算和Shader有一定了解的人员,当然你也可以直接使用成果。
网格使用代码生成,使得我们有更多可配置的余地
Unity中网格要可见还需要两个额外的好搭档Material和Shader,所以我们先创建好三个必备文件Lowpoly.cs(C#代码绘制网格)、Lowpoly.material(材质球)和LowpolyShader(着色器,用于给材质球着色)。
嗯,请脑补掉企鹅大厂的Logo : )
首先通过观察图片我们需要得知大致的绘制思路,大概如下几点:
我们就根据上述已经总结好的几点思路来逐步讲解原理
绘制NxM的网格
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;
}
计算法向量
严格上来说,一个顶点不可能有法线。但当使用Phong或Gouraud着色过程进行光照计算时,点法线提供了模拟光滑表面的一种方式。想象一个人体的多边形网格模型:这个模型只是一些多边形。但是这个网格模型能模拟一个人体。如果一个多边形里面的所有像素都使用相同的颜色着色,那么这个多边形看起来会非常平坦;但是通过使用点法线,我们能够对三角形的不同顶点应用不同的光照,这样就能够产生比较光滑的显示效果。(该段内容参考至:生成点法线(Generating Vertex Normals)) 又因为我们的效果中要求三个顶点的法向量相同,所以我们就没必要去计算法向量,直接用面法向量代替即可。如果你纠结顶点法向量的计算方法,点击使用这张飞机票 关于点法线向量的计算。
面法向量如何计算呢?组成一个面的两个向量进行叉积就是法向量,不明白的同学也可以看看关于点法线向量的计算。
按照下图两个向量的叉乘即可求出三角面的面法向量
计算面法向量参考图:
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.mesh = selectedGo.GetComponent().GenerateLowPoly();
}
}
然后再工具栏里找到我们创建的按钮,点击!然后双击游戏物体MeshFilter组件上Mesh就可以在Unity的右下角明确看到当前创建的网格的顶点个数和三角面个数和对模型的预览。
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();
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..
- Unity自定义UI组件(十一) 雷达图、属性图
- Unity自定义UI组件(十) 折线图
- Unity自定义UI组件(九) 颜色拾取器(下)
- Unity自定义UI组件(八) 颜色拾取器(上)
- Unity自定义UI组件(七)渐变工具、渐变色图片、渐变遮罩
- Unity自定义UI组件(六)日历、日期拾取器
- Unity自定义组件之(五) 目录树 UITree
- Unity自定义UI组件(四)双击按钮、长按按钮
- Unity自定义UI组件(三)饼图篇
- Unity自定义UI组件(二)函数图篇(下)
- Unity自定义UI组件(一)函数图篇(上)
- [Unity]PureMVC框架解读(下)
- [Unity]PureMVC框架解读(上)
- Github :https://github.com/ll4080333/UnityCodes
- CSDN : http://blog.csdn.net/qq_29579137
- 博客专栏 : http://blog.csdn.net/column/details/16329.html
- QQ群 : 593906968 有什么不懂的可以加群咨询互相学习
如果你想了解UGUI的更多拓展组件,欢迎关注我的博客,我会持续更新,支持一下我这个博客新手。如果以上文章对你有帮助,点个赞,让更多的人看到这篇文章,我们一起学习。如果有什么指点的地方欢迎在评论区留言,秒回复。