效果
动态创建长度固定的可调节弧度的面,效果如图:
需求
需要一个可以调节弧度的且长度固定的面(最多可调节至半圆型)。
从官方手册中可以知道,想要使用代码来创建Mesh需要至少四个步骤:
- 设置顶点数组(想要创建的面一共有多少个顶点)
- 设置三角形信息(每三个顶点确定一个三角形,要顺时针的设置)
- 设置法线信息(可以通过mesh的方法直接计算)
- 设置UV信息(决定了图像信息如何在此Mesh上显示,其设置顺序和顶点数组的设置顺序相同)
顶点数组
先讨论如何计算得到弧面上的顶点信息
由于需求的弧面是可以调节的(0°~180°),那么就分为两种情况讨论:
- 0°的弧面(也就是平面)
- 非0°的弧面
0°的弧面
要知道在模型中任何一个面都是由诸多三角形组成的,平面也不例外,一个平面至少由两个三角形组成。例如下图,该平面由2段四个三角形组成:
假设我们想要的平面宽高都为1,且平面位于XY轴平面上,那么其6个顶点分别为:
左上 (-0.5, 0.5)
左下 (-0.5, -0.5)
正上 (0, 0.5)
正下 (0, -0.5)
右上 (0.5, 0.5)
右下 (0.5, -0.5)
其计算公式也很简单,拿到左上点,再计算出每段的分量,就可得到顶点数组了,可以看到下面这段代码是以列为单位为数组赋值,每次循环赋值上下两个顶点。
// 该平面由几段构成
int segments_count = 2;
// 平面宽度
int planeWidth = 1;
// 平面高度
int planeHeight = 1;
// 平面每段的宽度值
float widthSetup = planeWidth * 1.0f / segments_count;
// 该平面需要的顶点数
int vertex_count = (segments_count + 1) * 2;
// 平面顶点数组
Vector3[] vertices = new Vector3[vertex_count];
for (int si = 0; si <= segments_count; si++)
{
float x = widthSetup * si;
vertices[vi++] = new Vector3(-planeWidth / 2 + x, planeHeight / 2, 0);
vertices[vi++] = new Vector3(-planeWidth / 2 + x, -planeHeight / 2, 0);
}
非0°的弧面
对于非0°的弧面,就会复杂一些。首先要弄清楚,在弧面宽度固定的情况下,0°弧面与非0°弧面的区别。
若一个平面在XY轴空间坐标系内,想要将其变成带有弧度的面,首先需要增加一个维度把XY轴空间坐标系变成XYZ轴空间坐标系,然后为弧面上的点设置不同的Z值。
从这两张图中可以清晰的看到,仅仅只有Z轴的区别。
再上一张图,如下:
由需求可知,
s
是固定的长度,若想要动态改变弧面的弧度,就需要调节
同时根据圆的性质,想要求出圆上任意一点的位置,有如下公式:
其中 r <script type="math/tex" id="MathJax-Element-9">r</script>是半径。
知道以上的信息后,写代码就很容易了,如下:
int segments_count = planesSeg;
int vertex_count = (segments_count + 1) * 2;
Vector3[] vertices = new Vector3[vertex_count];
int vi = 0;
// 普通平面步
float widthSetup = planeWidth * 1.0f / segments_count;
// 半径
float r = planeWidth * 1.0f / (Mathf.Sin(threshold / 2) * 2);
// 弧度步
float angleSetup = threshold / planesSeg;
// 余角
float coangle = (Mathf.PI - threshold) / 2;
// 弓形的高度
// https://zh.wikipedia.org/wiki/%E5%BC%93%E5%BD%A2
float h = r - (r * Mathf.Cos(threshold / 2));
// 弓形高度差值(半径-高度)
float diff = r - h;
for (int si = 0; si <= segments_count; si++)
{
float x = 0;
float z = 0;
// 阈值不为0时,根据圆的几何性质计算弧上一点
// https://zh.wikipedia.org/wiki/%E5%9C%86
x = r * Mathf.Cos(coangle + angleSetup * si);
z = r * Mathf.Sin(coangle + angleSetup * si);
vertices[vi++] = new Vector3(-x, planeHeight / 2, z - diff);
vertices[vi++] = new Vector3(-x, -planeHeight / 2, z - diff);
}
三角形信息
由官方手册可以,在设置三角形信息的时候需要注意三角形索引设置的顺序一定要是顺时针的。
在上面的步骤中,我们设置的顶点都是从上到下的。
0 2 4 6 ....
1 3 5 7 ....
在这里,我选择构建三角形索引的方式是1-0-3
和0-2-3
这样的方式,大码如下:
int indices_count = segments_count * 3 * 2;
int[] indices = new int[indices_count];
int vert = 0;
int idx = 0;
for (int si = 0; si < segments_count; si++)
{
indices[idx++] = vert + 1;
indices[idx++] = vert;
indices[idx++] = vert + 3;
indices[idx++] = vert;
indices[idx++] = vert + 2;
indices[idx++] = vert + 3;
vert += 2;
}
法线信息
法线以及切线信息可由下面的代码计算:
mesh.RecalculateBounds();
mesh.RecalculateNormals();
mesh.RecalculateTangents();
UV信息
关于UV如何工作的,请看这里。
此外,一定要记住UV信息是在XY空间坐标系中的,且符合下面:
(0, 1)左上角 (1, 1)右上角
(0, 0)左下角 (1, 0)右下角
而且,UV信息设置的顺序要和顶点设置的顺序相同,否则可能会得到翻转的画面,例如我在设置顶点时采用的是自上到下的方式设置,那么UV信息的设置也要采用自上到下的方式设置。
Vector2[] uv = new Vector2[vertices.Length];
float uvSetup = 1.0f / segments_count;
int iduv = 0;
for (int i = 0; i < uv.Length; i = i + 2)
{
uv[i] = new Vector2(uvSetup * iduv, 1);
uv[i + 1] = new Vector2(uvSetup * iduv, 0);
iduv++;
}
最终代码
private Mesh CreateArcSurface(float threshold)
{
float planeWidth = 1;
float planeHeight = 1;
int planesSeg = 64;
Mesh mesh = new Mesh();
int segments_count = planesSeg;
int vertex_count = (segments_count + 1) * 2;
Vector3[] vertices = new Vector3[vertex_count];
int vi = 0;
// 普通平面步
float widthSetup = planeWidth * 1.0f / segments_count;
// 半径
float r = planeWidth * 1.0f / (Mathf.Sin(threshold / 2) * 2);
// 弧度步
float angleSetup = threshold / planesSeg;
// 余角
float coangle = (Mathf.PI - threshold) / 2;
// 弓形的高度
// https://zh.wikipedia.org/wiki/%E5%BC%93%E5%BD%A2
float h = r - (r * Mathf.Cos(threshold / 2));
// 弓形高度差值(半径-高度)
float diff = r - h;
for (int si = 0; si <= segments_count; si++)
{
float x = 0;
float z = 0;
if (threshold == 0)
{
// 阈值为0时,按照普通平面设置顶点
x = widthSetup * si;
vertices[vi++] = new Vector3(-planeWidth / 2 + x, planeHeight / 2, z);
vertices[vi++] = new Vector3(-planeWidth / 2 + x, -planeHeight / 2, z);
}
else
{
// 阈值不为0时,根据圆的几何性质计算弧上一点
// https://zh.wikipedia.org/wiki/%E5%9C%86
x = r * Mathf.Cos(coangle + angleSetup * si);
z = r * Mathf.Sin(coangle + angleSetup * si);
vertices[vi++] = new Vector3(-x, planeHeight / 2, z - diff);
vertices[vi++] = new Vector3(-x, -planeHeight / 2, z - diff);
}
}
int indices_count = segments_count * 3 * 2;
int[] indices = new int[indices_count];
int vert = 0;
int idx = 0;
for (int si = 0; si < segments_count; si++)
{
indices[idx++] = vert + 1;
indices[idx++] = vert;
indices[idx++] = vert + 3;
indices[idx++] = vert;
indices[idx++] = vert + 2;
indices[idx++] = vert + 3;
vert += 2;
}
mesh.vertices = vertices;
mesh.triangles = indices;
// // https://answers.unity.com/questions/154324/how-do-uvs-work.html
Vector2[] uv = new Vector2[vertices.Length];
float uvSetup = 1.0f / segments_count;
int iduv = 0;
for (int i = 0; i < uv.Length; i = i + 2)
{
uv[i] = new Vector2(uvSetup * iduv, 1);
uv[i + 1] = new Vector2(uvSetup * iduv, 0);
iduv++;
}
mesh.uv = uv;
mesh.RecalculateBounds();
mesh.RecalculateNormals();
mesh.RecalculateTangents();
return mesh;
}
参考资料: