3D模型一般是由网格(Mesh)和纹理(Texture)两部分构成。那什么是网格?从概念上讲,网格是图形硬件用来绘制复杂内容的结构。它至少包含一组基于三维空间点的顶点,以及一组连接这些点的三角形(最基本的2D形状)。而网格是由这些三角形(直角等腰三角形)构成的表面。那什么又是纹理?纹理是应用于3D模型表面的图像,用于增加模型的细节和真实感。纹理可以是物体表面的图案、颜色或材质效果,如木材的纹理、皮肤的质感等。纹理映射(Texture Mapping)是将纹理图像映射到3D模型表面的过程,通过这个过程,可以使模型看起来更加逼真。
在Unity中我们制作3D游戏时,需要各种各样的模型,你可以选择用3DMax、Maya等建好模型后导入Unity,也可以选择在Unity中用代码程序化生成网格,然后再加上各种细节,从而生成模型。比如程序化生成地型等。下面我们会用Perlin噪声图去生成山洞地型。
1. 绘制三角形
要绘制网格,我们需要一个也具有MeshFilter和MeshRenderer组件的游戏对象。我们可以强制将这些组件添加到同一游戏对象中,方法是为其赋予RequireComponent具有两种组件类型作为参数的属性。
[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class CreateMesh : MonoBehaviour{ }
Mesh包含但不限于:
- 顶点坐标数组
- 三角形顶点顺序的索引数组
- 顶点在UV坐标系中的位置信息
[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class DrawTriangle : MonoBehaviour
{
private Mesh mesh;
private Vector3[] vertices;//顶点坐标数组
private int[] triangles;//三角形顶点绘制顺序数组
private void Start()
{
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
vertices = new Vector3[]
{
Vector3.zero, Vector3.right, Vector3.up,
};
triangles = new int[]
{
0, 1, 2
};
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.normals = new Vector3[] {
Vector3.back, Vector3.back, Vector3.back
};
}
}
三角形顺序
顺时针和逆时针:
当使用顺时针的顶点顺序去绘制三角形,这意味着三角形可以从外部去看见他们,而内部或者说是背面是不会被绘制的。而使用逆时针的顶点顺序去绘制三角形,表现是相反的。
法向量
法向量是一个单位长度的向量,它描述了如果你站在表面上时的局部向上方向。因此,这些向量直接指向远离表面的方向。因此,我们的三角形表面的法向量应该是Vector3.back,指向网格局部空间中 Z 轴负方向的下方。但如果没有提供法向量,Unity 默认使用前向向量,因此三角形会从错误的一侧被照亮的。
虽然定义表面的法向量才有意义,但网格会定义每个顶点的法向量。用于着色的最终表面法线是通过在三角形表面上插入顶点法线来找到的。通过使用不同的法向量,可以为平面三角形添加表面曲率的幻觉。这使得网格看起来是光滑的,而实际上它们是多面的。
Vector3在设置顶点位置后,我们可以通过将数组分配给网格的属性来将法线向量添加到顶点normals。 Unity 检查数组是否具有相同的长度,如果我们提供的法线向量数量错误,Unity 将失败并发出警告。
2. 绘制圆筒
[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class DrawCylinder : MonoBehaviour
{
private Mesh mesh;
private Vector3[] vertices;//顶点坐标数组
private int[] triangles;//三角形顶点绘制顺序数组
public int xSize = 20;
public int zSize = 20;
public float radius = 5f;
public float zLength = 5f;//z方向的长度
private void Start()
{
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
CreateShape();//绘制图形
UpdateMesh();//更新网格信息
}
void CreateShape()
{
vertices = new Vector3[(xSize + 1) * (zSize + 1)];
int i = 0;
float angle;
float xPos;
float yPos;
float zPos;
//填充顶点数组
for (int z = 0; z <= zSize ; z++)
{
for (int x = 0; x <= xSize; x++)
{
angle = 2f * Mathf.PI * x / xSize;
xPos = radius * Mathf.Cos(angle);
yPos = radius * Mathf.Sin(angle);
zPos = z * zLength;
vertices[i] = new Vector3(xPos, yPos, zPos);
i++;
}
}
//填充三角形顶点数组
triangles = new int[xSize * zSize * 2 * 3];
int vert = 0;
int tris = 0;
for (int z = 0; z < zSize; z++)
{
for (int x = 0; x < xSize; x++)
{
triangles[tris + 0] = vert + 0;
triangles[tris + 1] = vert + xSize + 1;
triangles[tris + 2] = vert + 1;
triangles[tris + 3] = vert + 1;
triangles[tris + 4] = vert + xSize + 1;
triangles[tris + 5] = vert + xSize + 2;
vert++;
tris += 6;
}
vert++;
}
}
void UpdateMesh()
{
mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
}
private void OnDrawGizmos()
{
if (vertices == null)
return;
for (int i = 0; i < vertices.Length; i++)
{
Gizmos.DrawSphere(vertices[i], .1f);
}
}
}
绘制圆筒的过程实际上相当于将一张A4纸从一端开始卷起,卷成一个圆筒。圆筒的绘制在于如何将各个顶点放在正确的位置,我们可以画一个圆去理解,如下图。
所以就可以计算出x、y的位置
xPos = radius * Mathf.Cos(angle); yPos = radius * Mathf.Sin(angle);
。
基于这个原理,我们就可以从A4纸的左下角开始从左到右、从下到上一排一排的绘制三角形,直至绘制成圆筒。下图是我从内部绘制的圆筒。
3. 利用Perlin函数绘制山洞地型
[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class DrawByPerlin : MonoBehaviour
{
private Mesh mesh;
private Vector3[] vertices;
private int[] triangles;
public int xSize = 25;
public int zSize = 30;
public float radius = 8f;//改变地形的半径
public float offset = 1000f;//改变地形的形状外观
public float perlinScale = 0.06f;//改变地形的平滑度
public float waveHeight = 6f;//改变地形的凸凹程度
public float zLength = 2f;//改变地形的长度
private void Start()
{
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
CreateShape();
UpdateMesh();
}
void CreateShape()
{
vertices = new Vector3[(xSize + 1) * (zSize + 1)];
int i = 0;
float angle = 0;
float xPos;
float yPos;
float zPos;
for (int z = 0; z <= zSize ; z++)
{
for (int x = 0; x <= xSize; x++)
{
angle = 2f * Mathf.PI * x / xSize;
xPos = radius * Mathf.Cos(angle);
yPos = radius * Mathf.Sin(angle);
zPos = z * zLength;
vertices[i] = new Vector3(xPos, yPos, zPos);
//Perlin 标度和偏移量为 Perlin 噪声创建 X 和 Z 值
float pX = (xPos * perlinScale) + offset;
float pZ = (zPos * perlinScale) + offset;
//圆柱体切面中心
Vector3 center = new Vector3(0, 0, zPos);
//使用perlin噪声和波高使顶点向内移动
vertices[i] += (center - vertices[i]).normalized * Mathf.PerlinNoise(pX, pZ) * waveHeight;
i++;
}
}
//填充顶点数组
triangles = new int[xSize * zSize * 6];
int vert = 0;
int tris = 0;
for (int z = 0; z < zSize; z++)
{
for (int x = 0; x < xSize; x++)
{
triangles[tris + 0] = vert + 0;
triangles[tris + 1] = vert + xSize + 1;
triangles[tris + 2] = vert + 1;
triangles[tris + 3] = vert + 1;
triangles[tris + 4] = vert + xSize + 1;
triangles[tris + 5] = vert + xSize + 2;
vert++;
tris += 6;
}
vert++;
}
}
void UpdateMesh()
{
mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
}
private void OnDrawGizmos()
{
if (vertices == null)
return;
for (int i = 0; i < vertices.Length; i++)
{
Gizmos.DrawSphere(vertices[i], .1f);
}
}
}