Unity3D是一款渲染功能非常强大、交互性能优越的游戏引擎,著名的《纪念碑谷》(图1)就是采用该引擎开发的。
图1. 游戏《纪念碑谷》用过Unity3D的读者知道,Unity中只提供了正方体、球形、圆柱体等基础形状的预设体,那么如何在Unity3D中产生任意形状的物体呢?下面就我个人的经验,以产生一个箭头为例,提供可能的几个思路。
一、物体拼接
这是最简单的方法,因为一个箭头可以由细长圆柱 + 圆锥拼接而成。
我们需要先在Unity3D中新建一个空的物体,命名为“Arrow”(图2)。
图2. 新建空的游戏物体然后我们创建一个圆柱体(3D Object -> Cylinder),但是这里没有圆锥体,怎么办?这就用到了Unity3D的Asset Store了,在商店中搜索“ConeCollider”,可以找到相应的免费资源(图3)。
图3. 商店中的“ConeCollider”点击下载,下载完成后导入,可以找到相应的模型ConeCollider(图4),把它拉进Hierarchy中即可。
图4. 建立圆锥体接下来将圆柱和圆锥都移动到“Arrow”的空物体处,成为其子物体,同时调整圆柱、圆锥的大小和位置直至合适,这样一个箭头就产生了(图5)。同时,可以将Arrow拉倒下方文件夹中,成为一个Prefab(预设体),这样后面再产生箭头就可以由该预设体产生了。
图5. 拼接完成的箭头二、动态产生网格拓扑
第一种方法虽然非常简单方便,但是只适用于产生一些简单性状的物体,同时不利于物体网格的压缩。这里介绍另一种非常实用的方法——在Unity3D中动态产生相应性状的网格拓扑,这种方法理论上可以产生任意你想要的性状。
讨论具体的方法之前,需要提到Unity3D中物体形状的网格拓扑。在Unity3D中,所有物体都是由一个个三角面组成,也许你会问那怎么会有圆柱、球形等形状?聪明的读者已经从图4中的圆锥得到答案,只要划分足够细,正多边形可以近似为圆形,古代的“割圆术”即是这个原理。图6给出了Unity3D中球的网格拓扑(这里用到了渲染网格的UCLA Wireframe Shader,同样可以在商店中免费下载使用)。
图6. Unity3D中球的网格拓扑可以看到,Unity3D中的物体都是由这样的三角形网格组成的,构成网格的信息包括两部分:顶点(vertices)和三角拓扑信息(即每一个三角形由哪些点组成,triangles)。那么只需要通过脚本更改物体网格的顶点与拓扑,就可以得到相应的形状了,这就是第二种产生任意形状物体的方法——动态产生网格拓扑。
举个简单的例子,比如我们要生成一个正方形,我们可以把它分解成三角形(可以分解成2个三角形、4个三角形……这里以两个三角形为例,图7)。
图7. 正方形示意图顶点坐标都有了,那么接下来是三角拓扑信息。左下角三角形组成的顶点为0,2,1(逆时针顺序,Unity3D中通常采用单面渲染逆,即时针可见,顺时针不可见),右下角三角形组成的顶点为1,2,3。这样triangles就是一个整数型数组{0,2,1,1,2,3}。将上述顶点和拓扑替换掉原来物体的网格信息,再重新计算法线方向就可以得到新的网格了。
相应的代码如下:
// 生成正方形的4个顶点
Vector3[] vertices = new Vector3[4];
vertices[0] = new Vector3(0, 0, 0);
vertices[1] = new Vector3(0, 0, 1);
vertices[2] = new Vector3(0, 1, 0);
vertices[3] = new Vector3(0, 1, 1);
// 生成正方形的三角拓扑信息
int[] triangle = new int[6] {0,2,1,1,2,3 };
// 获取物体的网格
Mesh mesh = GetComponent<MeshFilter>().mesh;
// 清除原有网格
mesh.Clear();
// 赋予网格新的顶点
mesh.vertices = vertices;
// 赋予网格新的拓扑信息
mesh.triangles = triangle;
// 网格重计算法线
mesh.RecalculateNormals();
将相应的脚本绑定在场景中的游戏物体上,运行结果见图8。
图8. Unity3D运行产生的正方形回到我们之前的例子——箭头,那么你需要动脑筋梳理出箭头的顶点坐标和拓扑信息应该是怎么样的了。图9给出了作者采用的一种方案,以12边形近似为圆。
图9. 12边形近似的箭头相应代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CreateArrow : MonoBehaviour {
public float ra = 0.3f,// 圆柱的半径
ha = 3,// 圆柱的高度
rb = 1,// 圆锥的半径
hb = 2;// 圆锥的高度
public int n = 12;// 正多边形的边数
// 初始化函数
void Start () {
Vector3[] vertices = new Vector3[3 * n + 1];// 模型的顶点
int[] triangle = new int[18 * n - 12];// 模型的三角形拓扑
for (int i = 0; i < n; i++)
{
// 圆柱的底面顶点
vertices[i] = new Vector3((float)(ra * Math.Cos(2 * Math.PI * i / n)), (float)(ra * Math.Sin(2 * Math.PI * i / n)), 0);
// 圆柱的顶面顶点
vertices[n + i] = new Vector3((float)(ra * Math.Cos(2 * Math.PI * i / n)), (float)(ra * Math.Sin(2 * Math.PI * i / n)), ha);
// 圆锥的底面顶点
vertices[2 * n + i] = new Vector3((float)(rb * Math.Cos(2 * Math.PI * i / n)), (float)(rb * Math.Sin(2 * Math.PI * i / n)), ha);
}
// 圆锥的顶点
vertices[3 * n] = new Vector3(0, 0, ha + hb);
// 生成三角拓扑信息
for (int i = 0; i < n - 2; i++)
{
triangle[3 * i] = 0;
triangle[3 * i + 2] = i + 1;
triangle[3 * i + 1] = i + 2;
triangle[3 * (n - 2) + 6 * n + 3 * i] = 2 * n;
triangle[3 * (n - 2) + 6 * n + 3 * i + 2] = 2 * n + i + 1;
triangle[3 * (n - 2) + 6 * n + 3 * i + 1] = 2 * n + i + 2;
}
for (int i = 0; i < n - 1; i++)
{
triangle[3 * (n - 2) + 6 * i] = i;
triangle[3 * (n - 2) + 6 * i + 2] = n + i;
triangle[3 * (n - 2) + 6 * i + 1] = n + i + 1;
triangle[3 * (n - 2) + 6 * i + 3] = n + i + 1;
triangle[3 * (n - 2) + 6 * i + 5] = i + 1;
triangle[3 * (n - 2) + 6 * i + 4] = i;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * i] = 2 * n + i;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * i + 2] = 3 * n;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * i + 1] = 2 * n + i + 1;
}
triangle[3 * (n - 2) + 6 * (n - 1)] = n - 1;
triangle[3 * (n - 2) + 6 * (n - 1) + 2] = n + n - 1;
triangle[3 * (n - 2) + 6 * (n - 1) + 1] = n;
triangle[3 * (n - 2) + 6 * (n - 1) + 3] = n;
triangle[3 * (n - 2) + 6 * (n - 1) + 5] = 0;
triangle[3 * (n - 2) + 6 * (n - 1) + 4] = n - 1;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * (n - 1)] = 2 * n + n - 1;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * (n - 1) + 2] = 3 * n;
triangle[3 * (n - 2) + 6 * n + 3 * (n - 2) + 3 * (n - 1) + 1] = 2 * n;
// 获取物体的网格
Mesh mesh = GetComponent<MeshFilter>().mesh;
// 清除原有网格
mesh.Clear();
// 赋予网格新的顶点
mesh.vertices = vertices;
// 赋予网格新的拓扑信息
mesh.triangles = triangle;
// 网格重计算法线
mesh.RecalculateNormals();
}
}
将上述代码中的n改成128后,即以128边形近似为圆,结果如图10。
图10. 12边形近似的箭头三、生成fbx模型文件
第二种方法的好处是方便灵活,但是模型的可复制性不强,比如需要很多个箭头,每次都要重复计算各个箭头的顶点坐标,程序的计算量一下子就上去了。那么,有没有什么办法可以让自己设计的网格模型成为可重复使用的模型呢?当然有!那就是生成fbx模型文件,也就是第一种方法用到的模型ConeCollider这样的文件。
生成fbx文件的方法很多,可以借助其他的建模软件(如3d Max, MAYA等),也可以自己使用Unity3D编写fbx文件。下面介绍利用Unity3D生成fbx文件的方法,需要用到一个dll外部库——WRP_FBXExporter.dll(百度搜索下载即可)。
将WRP_FBXExporter.dll移动到Assets文件夹下,引用中就会自动添加可以使用。相比第二种方法,只需要添加几行代码即可导出模型的fbx文件。
// 建立游戏物体的数组,当前只有一个
GameObject[] gameObjects = new GameObject[1];
// 将场景中的物体作为数组元素,“CreateFBX”是场景中游戏物体名称
gameObjects[0] = GameObject.Find("CreateFBX");
// 调用FBXExporter.ExportFBX,生成对应的FBX文件
FBXExporter.ExportFBX("", "Arraw" + n, gameObjects, true);
生成结果见图11,可以看到已经在Assets下生成了Arraw12.fbx。
图11. 生成相应的fbx文件对于FBXExporter.ExportFBX () 函数的参数详解见图12,分别对应fbx文件存放文件、fbx文件名、转化的游戏物体数组等选项。
图12. FBXExporter.ExportFBX () 参数详解参考资料
[1] https://assetstore.unity.com/?q=ConeCollider&orderBy=0(ConeCollider下载)
[2] https://assetstore.unity.com/packages/vfx/shaders/directx-11/ucla-wireframe-shader-21897(UCLA Wireframe Shader下载)
[3] https://blog.csdn.net/dong2016hong/article/details/54847235(WRP_FBXExporter.dll下载)
网友评论