在 Unity3D 里创建立方体几乎是每个接触 Mesh 的人都要过的第一道关。看着挺简单一个六面体而已但真到代码里你会发现麻烦的不是怎么画出六个面而是为什么我画出来的东西从外面看不见为什么它一面亮一面黑为什么贴图贴上去是歪的。这篇内容就把 Unity3D 创建立方体这件事从头拆到尾从前置概念、整体设计、参数推导一直写到一份能直接复制运行的脚本以及我在真机上踩过的各种坑。适合刚上手 Mesh 编程、想搞明白顶点和三角形到底怎么组织的人也适合已经能跑通但从没细究过绕序、法线、UV 取值的人。看完你至少能做到三件事手搓一个尺寸可调的立方体、看懂别人写的网格生成代码、在自己写的时候知道哪些地方容易出错。下面全是实操层面的话不绕弯子。1. 先想清楚为什么要手搓立方体1.1 内置 Cube 的边界在哪Unity 的 Hierarchy 面板里点右键3D Object 下面那个 Cube本质上就是引擎预先准备好的一个网格资源边长 1 单位、几何中心在原点、24 个顶点、12 个三角形外加一个 BoxCollider。多数场景下够用了拖进来、缩缩放、换个材质就完事。但只要你稍微往深一点做就会撞上它固定的这层壳。最典型的是尺寸问题。内置 Cube 的网格尺寸是写死的 1x1x1你想让它变成 2x1x0.5只能改 Transform 的 Scale。这本身没什么但 Scale 之后就带出两个副作用一是光照计算里法线的处理会依赖变换矩阵的逆转置非等比缩放在某些管线和某些 Shader 下会出现光照方向偏二是 BoxCollider 跟着缩放走如果你要按真实尺寸算物理碰撞参数就得自己反算一遍。另外它的 UV 是固定布局一个面上贴图 0 到 1 铺满你想让某个面平铺四遍或者六个面各贴不同的图内置网格就办不到只能靠材质或者顶点着色器绕。更要命的是程序化生成场景。比如你要做一个体素地形、一个由方块拼出的建筑结构、一个可用参数驱动的道具预览器这时候要的是在代码里生成任意数量、任意尺寸的立方体网格而不是从 Hierarchy 里拖。所以手搓立方体的真正价值不在于造一个方块而在于掌握一套可以任意扩展的网格生成模板方块只是最小的验证样本。1.2 Mesh 数据结构的最小认知Unity 的网格本质上是几个并行数组最核心的四组是顶点位置、三角形索引、法线、UV 坐标。顶点位置是 Vector3 数组记录每个顶点在模型空间下的坐标三角形索引是 int 数组每三个连续的数字表示一个三角形数字的值是顶点的下标法线是 Vector3 数组长度必须和顶点数组一致记录每个顶点朝外的方向光照就靠它UV 是 Vector2 数组同样和顶点一一对应告诉贴图采样时该取纹理的哪个位置。这里有个新手很容易忽略的点数组长度必须严格对齐。顶点数组是 24那法线数组和 UV 数组也必须是 24少一个 Unity 不会报错但那部分顶点会拿到默认值法线默认是零向量光照直接黑掉。三角形索引数组的长度必须是 3 的倍数因为是三个一组构成面。索引里的值必须落在 0 到顶点数减一之间越界会抛异常。还有一个更隐蔽的知识点赋值顺序。虽然 C# 里你先给 triangles 赋值再给 vertices 赋值代码也能跑但内部的重建逻辑是跟着最后一次赋值触发的。稳妥的写法是先设置顶点、法线、UV最后设置 triangles然后调用 RecalculateBounds 把包围盒刷一遍。如果你调用了 RecalculateNormals那它会用绕序重新算一遍法线把你手工写的法线全部覆盖这一点在需要硬边效果时必须注意。1.3 目标拆解这次要做成什么样把需求列清楚后面写代码才不会乱。一立方体的边长要能通过参数调节而不是写死。二几何中心要落在物体的原点方便对齐和旋转。三六个面要各自独立法线朝外不做平滑这样贴图和光照都是标准的硬边效果。四UV 每个面都完整铺一次 0 到 1。五脚本能在运行时生成也能在编辑器里手动触发方便调参数。这几条看着平常实际每一条都对应着后面一个设计决策。尤其是第三条直接决定了我们要用 24 个顶点而不是 8 个。把目标写下来接下来每一步取舍都有依据不会写着写着自己也糊涂。2. 整体设计24 个顶点是为了什么2.1 8 个共享顶点会带来哪些麻烦一个立方体在几何上有 8 个角点直觉上我们会觉得那顶点数组就该是 8 个索引数组写 36 个数字组 12 个三角形。这个方案确实能跑顶点数最少内存最省但它有两个致命问题。第一是法线。8 个顶点被三个面共用那这个顶点该朝哪个方向如果按位置算法线只能得到从立方体中心指向该顶点的对角线方向也就是三条棱的平均。渲染出来的效果就是一个被磨圆的立方体棱角处会有渐变过渡看着像块肥皂。这不是 bug这是共享顶点在法线上的必然结果。第二是 UV。同一个顶点被三个面共用它只能有一个 UV 坐标。但三个面各自需要不同的 UV 展开一个 UV 坐标不可能同时满足三个面的需求贴图必然错乱会出现一个面上纹理被拉伸、另一个面重复采样的现象。所以只要你想让贴图正常显示共享顶点这条路就走不通。结论就很清楚了24 个顶点每个面 4 个面与面之间不共用。代价是顶点数变成三倍但换来的是完全可控的法线和干净的 UV。对一个立方体来说这点开销可以忽略但设计思路上必须一开始就定死否则写到一半发现贴图错了再返工会很痛苦。2.2 顶点顺序与正面朝向的关系Unity 默认的渲染状态是背面剔除也就是只画正面背面不画。那什么算正面规则是从观察者方向看过去三个顶点的排列顺序是顺时针的这个三角形就正面朝向观察者。这条规则是整篇文章里最容易出错的地方。我见过太多人写的代码逻辑完全正确顶点坐标一个不差就是因为三角形索引顺手写成了逆时针结果从外面看立方体是空的从里面看反而能看到内壁。判断的时候有个小技巧假设你站在面的正前方看着它从左下角出发走到左上角再走到右上角这个路径如果是顺时针的那就是正面。索引写法上一个四边形拆两个三角形就是 0-1-2 和 0-2-3。我个人的建议是不要死记硬背每条边该从哪开始。每写完一个面先在脑子里过一遍站在外面看这个顺序是不是顺时针如果有疑虑就写个小测试把材质换成双面渲染如果从外面看是通透的那说明你这个面朝内了把第一个三角形的 0-1-2 改成 0-2-1第二个改成 0-3-2就反过来了。养成这个自查习惯比背口诀靠谱。2.3 每个面的局部坐标系怎么定六个面如果一个个手写顶点坐标一共 24 个 Vector3写起来烦改起来更烦边长一改全部作废。更好的做法是把每个面抽象成一个局部坐标系面有一个朝外的法线 n一个面内向右的方向 right一个面内向上的方向 up再加一个面中心 center。这样每个面的四个顶点都可以用 center 加上 right 和 up 的偏移算出来边长变了只需要改一个 h 值。具体公式是左下角 center - right * h - up * h左上角 center - right * h up * h右上角 center right * h up * h右下角 center right * h - up * h。这里的 h 是半边长。写成这样的好处是六个面共用一段代码只是 n、right、up 三个向量不同而已用一个长度为 6 的数组循环遍历就行代码量直接砍掉一大半。关键是怎么把 right 和 up 算出来。我的做法是这样的先拿到面的法线 n因为观察者在外面看所以视线方向 forward 就等于 -n。然后选一个绝对不能和法线平行的辅助方向当屏幕上方对于左右前后四个侧面辅助方向用世界的正上方向 Vector3.up对于顶面和底面法线本身就和 up 平行得换成 Vector3.forward。拿到 up 之后用 Vector3.Cross(up, forward) 就能算出 right。这个叉乘的顺序不能反反了得到的 right 会指向相反方向最终顶点顺序会变成逆时针整个面的正反就颠倒了。3. 参数推导从边长到 24 个坐标3.1 半边长 h 与面中心边长用 edgeLength 表示那半边长 h 就是 edgeLength 除以 2。为什么要用半边长因为顶点坐标是相对立方体中心对称的中心在原点的情况下一个面的中心位置就是法线乘 h。比如前面Z 面的中心就是 (0, 0, h)右面X 面的中心就是 (h, 0, 0)。这样写出来所有坐标都以 h 为基准边长改了一个变量全跟着变。这里顺带说一个实际使用的经验我一般会额外暴露一个是否按包围盒对齐的开关因为有些场景需要立方体的底面贴着地面而不是中心在原点。这时候可以在生成完之后把整个网格的所有顶点沿着某一轴平移 h或者在生成时用一个 originOffset 参数加进去。这个需求在做建筑或者摆件的时候很常见提前留好口子比事后改网格要省事得多。还有一点是数值精度。h 如果是浮点数比如 0.5那没问题但如果边长是 0.7h 就是 0.35这个在浮点里也不精确不过差个 1e-7 对渲染来说完全无感。真正要注意的是缩放为 0 的情况如果边长传进来是 0 或者负数生成出来的网格会退化成一个点或者翻转界面上什么也看不到。所以我一般在方法开头加一个 Mathf.Max(edgeLength, 0.0001f) 的保护避免参数误传导致的排查困难。3.2 六个面的 right、up、normal 组合表把六个面的三个向量列出来这张表是整份实现的核心。表中的 h 是半边长center 是法线乘 h。面normalforward-nup 辅助方向right Cross(up, forward)center前 (Z)(0,0,1)(0,0,-1)(0,1,0)(-1,0,0)(0,0,h)后 (-Z)(0,0,-1)(0,0,1)(0,1,0)(1,0,0)(0,0,-h)左 (-X)(-1,0,0)(1,0,0)(0,1,0)(0,0,-1)(-h,0,0)右 (X)(1,0,0)(-1,0,0)(0,1,0)(0,0,1)(h,0,0)上 (Y)(0,1,0)(0,-1,0)(0,0,1)(1,0,0)(0,h,0)下 (-Y)(0,-1,0)(0,1,0)(0,0,1)(-1,0,0)(0,-h,0)这张表如果直接抄能省掉大量推导时间。但抄完最好自己验算一个面比如验证 Z 面center 是 (0,0,h)right 是 (-1,0,0)up 是 (0,1,0)按前面公式左下角是 (0,0,h) (h,0,0) (0,-h,0) (h,-h,h)左上角是 (h,h,h)右上角是 (-h,h,h)右下角是 (-h,-h,h)。取前三个点算叉乘法线方向正好朝 Z说明绕序正确。实际写代码的时候我不建议把这些向量手写进数组而是用代码算给一个法线数组然后根据法线是否和世界的 up 平行来决定用哪个辅助方向。这样万一以后要扩展成不是标准轴向的面比如斜面逻辑还能复用。3.3 UV 与法线数组的填充规则UV 的填充相对机械每个面的四个顶点对应 (0,0)、(0,1)、(1,1)、(1,0)。要注意的是顺序必须和顶点的顺序完全对应也就是左下角对应 (0,0)左上角对应 (0,1)右上角对应 (1,1)右下角对应 (1,0)。如果 UV 和顶点顺序错位贴图会出现镜像或者旋转这种错很难一眼看出来因为立方体本身是对称的你只会觉得这个贴图怎么怪怪的。如果想让贴图在每个面上平铺多遍比如 4x4那就把 1 换成 4UV 数组写成 (0,0)、(0,4)、(4,4)、(4,0)。这比在材质里调 Tiling 更可控因为材质的 Tiling 是全局的所有用这个材质的面都会被影响而写在网格里可以精确控制到每个面的平铺次数。法线数组的填充更简单同一个面的四个顶点法线完全相同都等于该面的法线 n。这里千万别用 RecalculateNormals它会根据三角形的平均法线去算虽然对方块来说结果和硬边一致但会多花一次计算而且一旦你的顶点有细微偏差就会得到意料之外的结果。手工写死的法线是确定的、可预期的这也是程序化生成网格时应该坚持的原则。UV 和法线都填完之后再一次性设置三角形数组最后调 RecalculateBounds 更新包围盒。4. 落地实现一份可直接运行的生成脚本4.1 组件依赖与对象准备在场景里新建一个空物体命名随意比如 ProceduralCube然后给它挂上两个组件MeshFilter 和 MeshRenderer。这两个是必须的MeshFilter 负责持有网格数据MeshRenderer 负责把网格画出来。少了 MeshFilter网格数据没地方存少了 MeshRenderer有数据也不会渲染。脚本里我用 RequireComponent 特性把这两个组件锁死避免别人手动挂脚本时忘了加组件。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class CubeMeshBuilder : MonoBehaviour { [Header(几何参数)] [Tooltip(立方体边长单位与世界单位一致)] public float edgeLength 1f; [Header(附加选项)] public bool autoBuildOnStart true; public bool addBoxCollider false; private Mesh _mesh; private void Start() { if (autoBuildOn
阅读完成 · 觉得有帮助?