Probuider
前几天在做一个小项目的时候, 用到了 Unity 自带的一个包 ProBuilder 其中的 Arch 生成 1/4 圆.
挺好玩的, 可以在直接 Unity 中根据需要用 Mesh 定制生成图形, 而不用建模软件.
但是存在一个小问题, 就是在使用的时候他的中心点是在生成图形的左下角.
旋转的时候不符合我的需求, 我想要的是生成的时候旋转中心在圆心的位置, 所以准备自己定制一个.
目标
关于 Mesh 生成图形的原理可以参考这篇文章, 讲得虽然不算很详细, 但足够了解基本概念了.
目标是生成下面图中的一个 1/4 空心圆柱体
我们切换到 Wireframe 模式下, 可以看出它是有一个一个的顶点, 并通过一条条的直线连接起来. 那么我们如何确定这些顶点和线的位置呢?
小目标 - 生成一个面
其实很简单的, 我们一步一步慢慢来. 一次生成一整个会有点麻烦, 我们可以一面一面来. 只要生成了第一个面, 其他的面也是类似的方法生成就好.
在前面我们提到了我们要的是生成一个圆柱体, 圆柱体一个的重要性质就是可以由一个圆形叠加产生, 也就是只要我们生成一个圆形, 就完成了大部分的工作.
我们知道 3D 建模就是由一个一个的三角形组合成的, 所以我们要用三角形来模拟来一个空心的圆.
在 Probuilder 中生成这样一个空心圆柱体用的是 Arch, 它有几个参数, 分别是
\(\color{#1E90FF}{Radius}\) 半径, 圆心到最外圈的距离
\(\color{#1E90FF}{Thickness}\) 厚度, 圆心到最外圈的距离 - 圆心到最内圈的距离
\(\color{#1E90FF}{Depth}\) 深度
\(\color{#1E90FF}{NumberOfSides}\) 由多少个面组成, 面越多越平滑, 性能也越差
\(\color{#1E90FF}{DrawArchDegrees}\) 总共绘制的角度
\(\color{#1E90FF}{NumberOfSides}\) 中的面是指由两个三角形一头一尾拼成的梯形, 多个头大脚小的梯形拼在一起便成了我们需要的圆形.
原理已经知道了, 那下一步只要确定三角形顶点的位置就 OK 了. 至于如何确定三角形顶点的位置, 我们可以再看下这张图.
是不是瞬间清晰明了, 红线的交汇处就是圆心的位置, 数字则是每个顶点的编号.
我们假设圆心在原点, 数字 0-1 所在的线为 180 度线.\(\color{#1E90FF}{Increment}\) = \(\color{#1E90FF}{DrawArchDegrees}\)/\(\color{#1E90FF}{NumberOfSides}\) 就是线与线之间的角度. 每条线的角度可以由 \(\color{#1E90FF}{180-Increment*i}\) 得到. i 为第几条线.
线上的点可以由 \(\color{#1E90FF}{y = r* sinθ, y = r* cosθ}\) 得到.
- // 顶点坐标
- vertexList.Clear();
- float incrementAngle = DrawArchDegrees / NumberOfSides;
- // 小于等于是因为 n+1 条线才能组成 n 个面
- for (int i = 0; i <= NumberOfSides; i++)
- {
- float angle = 180 - i * incrementAngle;
- float innerX = (Radius - Thickness) * Mathf.Cos(angle * Mathf.Deg2Rad);
- float innerY = (Radius - Thickness) * Mathf.Sin(angle * Mathf.Deg2Rad);
- vertexList.Add(new Vector3(innerX, innerY, 0));
- float outsideX = Radius * Mathf.Cos(angle * Mathf.Deg2Rad);
- float outsideY = Radius * Mathf.Sin(angle * Mathf.Deg2Rad);
- vertexList.Add(new Vector3(outsideX, outsideY, 0));
- }
在上面的代码中我们已经计算出了顶点的位置, 下一步我们要做的是按顺序插入三角形顶点的位置. 从 Mesh 这篇文章中我们可以知道, 只有是三角形是正面的情况下才会被渲染.
而正反面可以通过法线的朝向进行判断, 向外的面就是正面, 相反的就是背面.
在 Unity 中, 法线的朝向可以由左手法则得到. 拿出左手, 伸直, 拇指与其他四个指头垂直, 然后四指弯曲, 指尖朝向循环的方向, 拇指就指向法线的方向.
也就是说在上图中, 我们想渲染三角形, 顺序应该是类似这样的 012,321, 234, 543.
- // 三角形索引
- triangleList.Clear();
- int direction = 1;
- for (int i = 0; i <NumberOfSides * 2; i++)
- {
- int[] triangleIndexs = getTriangleIndexs(i, direction);
- direction *= -1;
- for (int j = 0; j < triangleIndexs.Length; j++)
- {
- triangleList.Add(triangleIndexs[j]);
- }
- }
\(\color{#F08080}{getTriangleIndexs}\) 代码如下
- int[] getTriangleIndexs(int index, int direction)
- {
- int[] triangleIndexs = new int[3] { 0,1,2};
- for (int i = 0; i < triangleIndexs.Length; i++)
- {
- triangleIndexs[i] += index;
- }
- if (direction == -1)
- {
- int temp = triangleIndexs[0];
- triangleIndexs[0] = triangleIndexs[2];
- triangleIndexs[2] = temp;
- }
- return triangleIndexs;
- }
至于 uv 坐标就更简单了, 把内圈顶点 uv 坐标中的 Y 固定为 0, 外圈顶点 uv 坐标中的 Y 固定为 1, 而 x 坐标由 \(\color{#1E90FF}{1/NumberOfSides}\) 得到:
- //UV 索引
- uvList.Clear();
- for (int i = 0; i <= NumberOfSides; i++)
- {
- float angle = 180 - i * incrementAngle;
- float littleX = (1.0f / NumberOfSides) * i;
- uvList.Add(new Vector2(littleX, 0));
- float bigX = (1.0f / NumberOfSides) * i;
- uvList.Add(new Vector2(bigX, 1));
- }
完整代码如下:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- //[RequireComponent(typeof(MeshFilter))]
- //[RequireComponent(typeof(MeshRenderer))]
- //[ExecuteInEditMode]
- public class DrawArch : MonoBehaviour
- {
- public float Radius = 20.0f; // 外圈的半径
- public float Thickness = 10.0f; // 厚度, 外圈半径减去内圈半径
- public float Depth = 1.0f; // 厚度
- public float NumberOfSides = 30.0f; // 由多少个面组成
- public float DrawArchDegrees = 90.0f; // 要绘画多长
- public Material archMaterial = null;
- private List<Vector3> vertexList = new List<Vector3>();
- private List<int> triangleList = new List<int>();
- private List<Vector2> uvList = new List<Vector2>();
- // Start is called before the first frame update
- void Start()
- {
- GenerateVertex();
- }
- void GenerateVertex()
- {
- // 顶点坐标
- vertexList.Clear();
- float incrementAngle = DrawArchDegrees / NumberOfSides;
- // 小于等于是因为 n+1 条线才能组成 n 个面
- for (int i = 0; i <= NumberOfSides; i++)
- {
- float angle = 180 - i * incrementAngle;
- float innerX = (Radius - Thickness) * Mathf.Cos(angle * Mathf.Deg2Rad);
- float innerY = (Radius - Thickness) * Mathf.Sin(angle * Mathf.Deg2Rad);
- vertexList.Add(new Vector3(innerX, innerY, 0));
- float outsideX = Radius * Mathf.Cos(angle * Mathf.Deg2Rad);
- float outsideY = Radius * Mathf.Sin(angle * Mathf.Deg2Rad);
- vertexList.Add(new Vector3(outsideX, outsideY, 0));
- }
- // 三角形索引
- triangleList.Clear();
- int direction = 1;
- for (int i = 0; i <NumberOfSides * 2; i++)
- {
- int[] triangleIndexs = getTriangleIndexs(i, direction);
- direction *= -1;
- for (int j = 0; j < triangleIndexs.Length; j++)
- {
- triangleList.Add(triangleIndexs[j]);
- }
- }
- //UV 索引
- uvList.Clear();
- for (int i = 0; i <= NumberOfSides; i++)
- {
- float angle = 180 - i * incrementAngle;
- float littleX = (1.0f / NumberOfSides) * i;
- uvList.Add(new Vector2(littleX, 0));
- float bigX = (1.0f / NumberOfSides) * i;
- uvList.Add(new Vector2(bigX, 1));
- }
- Mesh mesh = new Mesh()
- {
- vertices = vertexList.ToArray(),
- uv = uvList.ToArray(),
- triangles = triangleList.ToArray(),
- };
- mesh.RecalculateNormals();
- gameObject.AddComponent<MeshFilter>().mesh = mesh;
- gameObject.AddComponent<MeshRenderer>().material = archMaterial;
- }
- int[] getTriangleIndexs(int index, int direction)
- {
- int[] triangleIndexs = new int[3] { 0,1,2};
- for (int i = 0; i < triangleIndexs.Length; i++)
- {
- triangleIndexs[i] += index;
- }
- if (direction == -1)
- {
- int temp = triangleIndexs[0];
- triangleIndexs[0] = triangleIndexs[2];
- triangleIndexs[2] = temp;
- }
- return triangleIndexs;
- }
- }
未完待续...
来源: https://www.cnblogs.com/suanjie/p/11870178.html