ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity陶艺模拟实战:顶点级网格形变与拉坯算法详解

Unity陶艺模拟实战:顶点级网格形变与拉坯算法详解 简介一份面向Unity开发者的陶艺制作模拟工程示例聚焦动态网格技术演示陶器拉坯过程中模型实时成形与表面平滑的实现思路。资源以7z格式打包共43个文件主要包含Unity场景、材质、脚本及工程配置asset文件构成主要资源mat定义陶瓷材质cs脚本承载动态模型生成与顶点调整逻辑json用于包管理和项目配置压缩包仅56KB结构紧凑适合直接阅读源码。项目覆盖Unity动态生成模型、动态调整模型顶点、动态平滑法线三个关键技术点场景中结合了拉坯交互所需的基础流程便于理解从网格生成到顶点编辑、再到法线重算的完整链路。已有2500人学习/下载适合具备一定Unity基础、希望掌握程序化建模与Mesh顶点级操作的初中级开发者。通过工程内的脚本与场景组织方式可快速复用核心逻辑并在此基础上扩展陶瓷造型或捏塑交互。1. 陶艺模拟软件在 Unity 里到底建什么模一张每帧都在被捏的网格Unity 陶艺制作模拟软件这个名字听起来像「用 Unity 做个展示陶瓷模型的场景」实际动手做一次就会明白真正难的不是材质不是灯光而是让泥巴像泥巴一样被捏、被拉、被刮。陶艺模拟的体验核心是转台上一块泥坯用户的手指或工具压上去泥面要实时凹陷、隆起、延展松手后形状还要稳定住。这个需求在 Unity 里落地本质上就是做一套「顶点级网格形变系统」每帧遍历 mesh 顶点根据工具头的位置和移动方向去修改顶点坐标再把法线重算回来。它适合两类人一是做陶瓷设计辅助工具或 VR 陶艺体验的团队二是想在 Unity 里做可塑性物体交互的开发者。这类项目不依赖第三方建模软件不依赖高精度扫描资产核心算法自己写完全可控今天就按这个方向拆开讲。2. 捏泥巴的形变算法从射线命中点到衰减位移这是整套系统地基2.1 准备一个能承受多次捏压的网格重建圆柱体而不是直接用默认 CylinderUnity 自带的 Cylinder 网格顶点数极低侧面只有 33 个顶点左右拿来当泥坯捏一下就会出现明显的棱边。陶艺模拟的第一件事是重建泥坯网格我需要足够的径向分段来让圆周看起来圆滑足够的高度分段来让上下捏压有过渡。我一般用径向 64 段、高度 32 段的 OpenGL 风格圆柱体顶点总量在 2112 个左右这个数量在 CPU 上做实时形变是毫无压力的。注意顶点必须用共享方式生成也就是径向收尾处要有一列重复顶点用于 UV 缝合这块在后面讲拉坯时还会用到。using UnityEngine; public static class ClayMeshBuilder { public static Mesh BuildCylinder(int radialSegments, int heightSegments, float radius, float height) { Mesh mesh new Mesh(); int capVerts (radialSegments 1) * 2; int sideVerts (radialSegments 1) * (heightSegments 1); Vector3[] verts new Vector3[capVerts sideVerts]; Vector2[] uvs new Vector2[verts.Length]; int[] tris new int[radialSegments * heightSegments * 6 radialSegments * 2 * 3]; int offset 0; // 顶部圆心 侧壁顶点按行列填充 for (int h 0; h heightSegments; h) { float y h * height / heightSegments - height * 0.5f; for (int r 0; r radialSegments; r) { float angle r * Mathf.PI * 2f / radialSegments; int idx offset h * (radialSegments 1) r; verts[idx] new Vector3(Mathf.Cos(angle) * radius, y, Mathf.Sin(angle) * radius); uvs[idx] new Vector2(angle / (Mathf.PI * 2f), h / (float)heightSegments); } } // 三角形索引的填充省略保证顶底封口即可 mesh.vertices verts; mesh.uv uvs; mesh.RecalculateNormals(); return mesh; } }这段代码的关键点有两个uv.x 存的是圆周角度归一化到 0~1 的值后面做旋转对称形变时可以直接用这个分量判断顶点在哪个角度带uv.y 存的是高度比例按高度做收口提拉时也用得上。radialSegments 和 heightSegments 不要一开始就拉到 256先在 64/32 下调通逻辑确认性能瓶颈不在网格量之后再往上加。2.2 压泥算法实现这一小步是整套陶艺模拟的地基捏泥巴的手感核心是工具尖端在移动过程中对落在影响半径内的顶点施加一个沿移动方向的推力同时推力随距离衰减。这里最容易翻车的是坐标系混用。顶点从 mesh.vertices 取出来时是局部坐标而工具尖端 handTip.position 是世界坐标直接用两者算距离会得到完全错误的结果。低阶做法是先把顶点用 TransformPoint 转到世界坐标算距离再把位移方向用 InverseTransformDirection 转回局部坐标后叠加到顶点上。using UnityEngine; public class ClayPinch : MonoBehaviour { public MeshFilter clayMesh; public Transform toolTip; // 手指或工具尖端 public float radius 0.06f; // 影响半径单位米 public float force 0.012f; // 单帧最大位移量单位米 public AnimationCurve falloff AnimationCurve.EaseInOut(0f, 1f, 1f, 0f); private Mesh mesh; private Vector3[] verts; private Vector3 lastTipPos; void Start() { mesh clayMesh.mesh; verts mesh.vertices; lastTipPos toolTip.position; } void Update() { Vector3 tipPos toolTip.position; Vector3 moveDir tipPos - lastTipPos; float moveMag moveDir.magnitude; if (moveMag 1e-6f) return; moveDir / moveMag; for (int i 0; i verts.Length; i) { Vector3 worldV clayMesh.transform.TransformPoint(verts[i]); float dist Vector3.Distance(worldV, tipPos); if (dist radius) continue; float t 1f - Mathf.Clamp01(dist / radius); float weight falloff.Evaluate(t); Vector3 displacement moveDir * (force * weight); verts[i] clayMesh.transform.InverseTransformDirection(displacement); } mesh.vertices verts; mesh.RecalculateNormals(); lastTipPos tipPos; } }逻辑说明moveMag 小于阈值时直接 return避免工具静止时每帧都重复算一遍全顶点循环。falloff 曲线用 EaseInOut 能让中心区域位移更集中、边缘过渡更柔和捏出来的坑不会出现硬边界。force 是单帧位移量不是总位移量因此按住工具不动不会继续压深想做出「按压持续下陷」的效果需要额外检测移动距离为零但按下状态为真时的深度累积。这个脚本算的是世界空间距离所以泥坯物体缩放不能为 0否则 TransformPoint 和 InverseTransformDirection 会互相抵消出问题。2.3 手感从哪来力度、半径、衰减曲线三个参数怎么配参数调优的优先级很明确先定 radius再调 force最后动 falloff 曲线。radius 直接决定捏一下影响多大面积做小件陶器时常用 0.03~0.05 米做大花瓶时可以用 0.08~0.12 米。force 决定单次捏压的深浅0.008~0.015 是常见区间超过 0.02 会出现明显顶点堆积泥面像被铲起来而不是被按下去。falloff 曲线的形状决定边界是否柔和EaseInOut 是最稳妥的起点想要「指腹按压」的钝感就调成先平后陡想要「指甲划痕」的锐利感就用先陡后平。这里有一个值得提醒的点不要用 PerlinNoise 或随机扰动直接叠加到顶点位移上做「泥土细节」。捏泥是确定性交互用户的手指移动方向是唯一的输入随机扰动会让同一个动作捏出不同形状这在陶艺软件里是致命的。随机细节应该放在表面纹理层也就是后面讲的顶点色通道方案而不是污染形变数据。3. 转台与拉坯把角度写进 UV 坐标做旋转对称的实时形变3.1 拉坯网格的 UV 极坐标生成陶艺区别于自由雕塑的最大特征是转台。真实拉坯时泥坯绕 Y 轴旋转手指固定在一个位置泥面就会被切削出旋转对称的形状。在 Unity 里复现这个行为最朴素的做法是让泥坯每帧绕 Y 轴转一个角度同时手指不动用前面那套 Pinch 逻辑逐帧处理——但这样网格会累积「螺旋形」的形变痕迹因为每帧作用在不同角度带上。正确做法是形变计算走对称规则转台只做视觉显示。我在做拉坯网格时会把 UV.x 直接当成角度带编号。上一节建的圆柱体已经是这样生成的读者如果要从外部模型导入需要自己重建 UV将顶点绕 Y 轴的反正切值归一化到 0~1 写入 uv.x把高度比例写入 uv.y。void RebuildPolarUV(Mesh mesh, bool seamAtBack) { Vector3[] verts mesh.vertices; Vector2[] uvs new Vector2[verts.Length]; for (int i 0; i verts.Length; i) { float angle Mathf.Atan2(verts[i].z, verts[i].x); if (angle 0f) angle Mathf.PI * 2f; float u angle / (Mathf.PI * 2f); // seamAtBack 让接缝处在 z 轴负方向用户视角通常看不到 if (seamAtBack) u Mathf.Repeat(u 0.5f, 1f); uvs[i] new Vector2(u, Mathf.InverseLerp(mesh.bounds.min.y, mesh.bounds.max.y, verts[i].y)); } mesh.uv uvs; }这段代码的边界情况很典型Atan2 返回 [-π, π]负角度要加 2π 才能变成 0~1 的连续 UV否则接缝处会从 0.98 直接跳到 0.02做对称形变时那个角度带的顶点永远连不上。接缝位置放在背面是行业共识因为转台旋转时用户盯着正面接缝处如果有轻微法线不连续也不容易被察觉。3.2 让同一圈顶点联动对称形变的位移规则拉坯的形变规则和自由捏压完全不同。以「收口」为例手指在某个角度位置向圆心方向压真实陶轮上一圈泥都会被推向圆心形成一个均匀的收口。实现方式是为所有处于同一角度影响范围内的顶点计算同一个位移量而不是像捏压那样按顶点到工具实际距离衰减。void ApplySymmetricSqueeze(Mesh mesh, float toolAngleDeg, float radiusDeg, float squeezeAmount) { Vector3[] verts mesh.vertices; Vector2[] uvs mesh.uv; for (int i 0; i verts.Length; i) { float vertAngleDeg uvs[i].x * 360f; float angleDiff Mathf.Abs(Mathf.DeltaAngle(vertAngleDeg, toolAngleDeg)); if (angleDiff radiusDeg) continue; float t 1f - angleDiff / radiusDeg; float weight Mathf.SmoothStep(0f, 1f, t); Vector3 localPos verts[i]; Vector3 radialDir new Vector3(localPos.x, 0f, localPos.z).normalized; // 向圆心收缩y 方向略微抬升模拟泥被挤上去 verts[i] localPos - radialDir * (squeezeAmount * weight) Vector3.up * (squeezeAmount * weight * 0.5f); } mesh.vertices verts; mesh.RecalculateNormals(); }关键点在于 Mathf.DeltaAngle。它处理的是角度差值跨越 0/360 边界的情况UV.x 为 0.99 和 0.01 的两个顶点实际角度差只有 7.2 度而不是 352.8 度用普通绝对值相减会让接缝处永远出现一条不参与形变的死区。squeezeAmount 控制单次收口量真实拉坯时一次收口不超过 0.01 米需要多次重复才能把口收小这和物理上泥坯不能一次形变过大的直觉一致。3.3 转台显示与数据分离不要让 mesh 跟着物理转台旋转转台的视觉旋转这里有一个极具误导性的做法直接把 clayMesh.transform.Rotate 让整个网格转起来这样形变数据也会跟着转用户上一次捏的位置在下一帧就跑掉了。正确做法是保留一个独立的转台对象做视觉旋转泥坯网格本身不转但顶点数据按「转台角度偏移」来做对称变换。也就是说用户在屏幕上看到转台带着泥坯转动但实际参与形变计算的网格角度是相对静止的。我的做法是维护一个 float wheelAngle每帧根据转台转速累加然后把工具的世界角度减去 wheelAngle 后传入对称形变函数。这样物理转台只负责显示网格数据永远处于本地静止坐标系。摄像机跟随转台旋转时用平滑阻尼而不是直接 LookAt否则每帧 RecalculateNormals 产生的微小法线抖动会被镜头放大操作时画面会「呼吸」。3.4 拉坯工具的参数配置拉坯时常用的三个参数是转速、手指停留角度带宽度和单次收缩量。转速一般 15~30 度每秒太慢用户失去「旋转对称」的心理预期太快则形变来不及累加。radiusDeg 影响的是角度带宽我通常设 15~25 度太小收口不均匀太大则形变过于平缓。单次收缩量压到 0.005~0.01 米和真实陶艺一样「多走几圈」比「一次到位」更容易控制形状。拉坯的提拉动作是把收口逻辑反过来工具在某高度带停留泥坯旋转时顶部顶点向上移动并略微向圆心收。这一步用 uv.y 判断高度带与角度带做二维加权实现方式和 SymmetricSqueeze 同构区别只是位移方向从径向改成 y 轴正向。4. 除了捏还要刮多种陶艺工具的算法差异与手感接入4.1 捏、刮、拉三种工具的位移规则差别陶艺软件做到能「捏」只算完成一半修坯和刮削才是让作品从粗糙变精致的步骤。从算法上讲捏是向移动方向推进顶点拉是沿切线方向拉伸顶点刮则是把高于工具平面的顶点整体削平。刮削的位移规则不依赖工具移动方向只依赖工具平面的法线方向。我把三种工具定义成同一个 CeramicTool 资产里的枚举运行时按类型分发到不同的位移函数。public enum ToolType { Pinch, Scrape, Pull } public class CeramicTool : ScriptableObject { public ToolType type; public float radius 0.06f; public float force 0.012f; public AnimationCurve falloff AnimationCurve.EaseInOut(0f, 1f, 1f, 0f); public bool usePressure true; }刮削的核心是一行数学判断顶点与工具平面的有向距离大于零才被修正小于零保持不变。这个「半空间裁剪」逻辑写起来简单但很容易忽略一个坑——工具平面不能只用法线判断还要限制工具头的空间范围。否则一个平面扫过去会把整个圆周上所有高出的顶点一刀切平这不符合刮刀只修局部的要求。void ApplyScrape(Vector3 toolPos, Vector3 toolNormal, float toolRadius) { for (int i 0; i verts.Length; i) { Vector3 worldV transform.TransformPoint(verts[i]); if (Vector3.Distance(worldV, toolPos) toolRadius) continue; float signedDist Vector3.Dot(worldV - toolPos, toolNormal); if (signedDist 0f) { Vector3 flattened worldV - toolNormal * signedDist; verts[i] transform.InverseTransformPoint(flattened); } } mesh.vertices verts; mesh.RecalculateNormals(); }signedDist 大于零说明顶点在工具平面推进方向的外侧需要被削回平面小于零说明顶点已经低于工具平面不做处理。toolNormal 必须是工具在当前帧的朝向如果工具朝向了错误方向刮削会变成反向膨胀。调试时可以在工具尖端画一个小平面的 Gizmos 来表示当前切削平面肉眼比对最直接。4.2 用鼠标模拟压感Input System 里 pressure 动作的接入方式压感是陶艺手感的核心变量。真实捏泥时按压力度决定形变深度Unity 里要复现这个反馈成熟方案是用 Input System 的 pressure 动作读取数位板笔压或触控压力。鼠标没有压力值所以要做一层「伪压感」按住左键时间越长压力越大或者用滚轮直接调整力度系数。我一般把压感设计成两层底层是工具自身的 force 参数上层是当前输入压力系数 0~1两者相乘得到实际单帧位移量。用鼠标时按住左键开始累积 pressTime用 Mathf.Clamp01(pressTime / 0.5f) 模拟从轻到重的按压过程用数位板时直接读 Pointer.pressure配套的压感曲线用 AnimationCurve 配置把 0~1 压力映射到 0~1 位移权重。这样同一个工具资产在鼠标和数位板下都能工作只是乘上的系数来源不同。拉坯软件里还会有旋转台踏板的模拟输入转速和压感一样做成可调系数而不是直接暴露转台角度值。原因是用户在拉坯时注意力集中在泥坯上不会去看转速数值需要的是「踩踏板越深转得越快」的直觉联动。4.3 把工具预设做成 ScriptableObject 资产换手感不用改代码工具参数的组合非常多半径 0.04 的细刮刀、半径 0.1 的粗压掌、收口专用的对称拉坯工具……如果每个参数都在 Inspector 里手调换工具时就得重新配置一遍。把每种工具做成 ScriptableObject 资产后文件系统里每个 .asset 就是一把虚拟工具运行时根据当前选中工具加载参数即可。[CreateAssetMenu(fileName CeramicTool, menuName Pottery/Tool)] public class CeramicTool : ScriptableObject { public ToolType type; public string displayName; public float radius; public float force; public AnimationCurve falloff; public bool usePressure; public float pressureCurveExponent 1f; }配套一个 ToolManager 单例或纯静态类持有当前工具引用Input 层只负责转发位移方向与压力系数具体用哪套半径和 falloff 由工具资产自己决定。这个方案的好处是换手感不用改代码策划或美术同学在 Project 窗口复制一份资产改参数就能得到新工具。这个方向可以复用同样的思路工具参数天然适合序列化后续做保存/加载时直接把工具列表序列化成 JSON 也是顺理成章的事。5. 陶艺模拟的 5 个常见坑顶点撕裂、卡顿、水印、压感和撤销5.1 形变后顶点撕裂和法线断层现象捏出来的坑边缘在旋转视角下出现锯齿状棱边或者拉坯后侧面出现一条明显硬边泥面像被掰断而不是被捏弯。原因网格精度不足以承载大梯度形变或者 RecalculateNormals 在角度接缝处算出了方向突变。接缝处两个 UV 角度差为 0.99 和 0.01但几何位置相邻法线平均时如果三角形面片在这条边上有折叠就会产生断裂感。解决先检查 radialSegments 是否低于 48太低就加大到 64 以上再检查形变函数的衰减带是否过窄收口时角度带宽小于 10 度就会出棱边。最后处理接缝在 BuildCylinder 时把接缝处的顶点拆成两个位置相同但 UV 不同的顶点这样法线在接缝处独立计算不会跨接缝平均。5.2 顶点数膨胀与 GC 压力现象顶点数加到 1 万以上后捏一下卡一下Realtime Profiler 里看到 Update 耗时飙到 20ms 以上。原因TransformPoint 和 InverseTransformDirection 在循环里每帧为每个顶点分配临时 Vector3加上 RecalculateNormals 是全量重算CPU 和 GC 双双过载。解决第一优先是限制顶点总量陶瓷器物 1.5 万顶点以内足够圆滑第二优先是只重算受影响区域的法线。做法是把形变影响的三角形索引收集到一个 HashSet 里只对这些三角形的法线做加权平均。第三个优化是顶点数据用 NativeArray 存配合 Job System 做并行位移计算这一步能扛住 3 万顶点以上的实时形变但前期不需要先把全量重算法线的问题解决掉。5.3 Unity trial version 水印到底影响什么现象编辑器 Game 视图右下角一直有 Unity 官方试用版水印截图和录屏全被盖上发给别人看效果很掉价。原因Unity 许可证没有激活或者在用未激活的试用模式跑编辑器。解决打开 Edit → Manage License登录 Unity 账号后按项目规模选 Personal 或 Pro 许可。个人开发者和年营收低于门槛的小团队用 Personal 免费许可即可不需要付费授权激活后编辑器水印会消失。构建出的 Windows/Mac/Linux/Android/iOS 产物默认不带这个编辑器水印但如果你用编辑器内置录屏功能录制演示视频水印会录进去。这里特别提醒不要用非官方渠道的破解激活工具它们常改坏 Unity 的 License 文件导致后续版本升级时反复出现激活失败浪费时间。5.4 新旧输入系统切换导致压感和旋转失效现象项目从旧项目升级到 Unity 2022 以上后鼠标点击还能移动工具但压力值永远为 0转台旋转也时灵时不灵。原因Project Settings → Player → Active Input Handling 被切成了 Input System 模式旧 API 的 Input.GetMouseButton 和 Input.GetAxis 被禁用而代码还在用旧 API 读输入。解决统一到新输入系统用 UnityEngine.InputSystem 的 Pointer.current.pressure 读取压感用键盘 Action 的 performed 事件控制转台转速。如果项目还挂着一部分旧代码没迁移可以在 Active Input Handling 里选 Both 模式兼容两套 API但不要长期依赖这个模式它会让输入处理多走一层转换设备多时会有偶发抖动。这时就需要自定义 ENABLE_LEGACY_INPUT_MANAGER 宏来精确控制哪些脚本走旧 API 哪些走新 API。5.5 撤销重做全量快照太贵增量记录太麻烦现象用 Undo.RegisterFullObjectHierarchyUndo 做撤销捏十几下之后每次撤销卡顿一两秒用户直接失去耐心。原因每次操作都把这个含 2 万顶点数组的 mesh 完整快照一遍Undo 栈里存了几十份完整顶点数组内存和序列化开销全爆了。解决自定义增量撤销系统。每次捏压或刮削操作完成后记录「被修改的顶点索引 修改前的位置数组」撤销时只恢复这些顶点。一次捏压通常只影响几百个顶点一个顶点三个 float几百个顶点才几千字节比全量快照小一个数量级以上。注意在指针抬起时提交一步撤回记录而不是每帧提交否则一次拖动会产生几十条记录撤销要按几十下才能回到起点。6. 进阶技巧把湿度写进顶点色通道模拟干裂与烧制变色陶艺除了形变还有一个物理过程被很多模拟软件忽略湿度。真实泥坯从湿到干表面会从深色变浅干透后开始出现裂纹烧制后颜色又会变化。这套逻辑如果做成独立子系统会非常重但利用已有的顶点数据可以很轻地模拟顶点色的 r 通道存湿度g 通道存釉料覆盖量b 通道可以留给后续扩展。干裂的几何效果不需要真的拆面。常见做法是在顶点位移上叠加一个「裂纹扰动」当某顶点湿度低于阈值且周围湿度梯度较大时让该顶点沿法线方向缩进 0.002~0.005 米同时在其 UV 上叠加一层 Voronoi 噪点纹理作为裂纹线。这样泥面看起来有细小的凹缝但网格拓扑没有被破坏性能和稳定性都可控。真正的拓扑撕裂只用在少数几个明显的长裂缝上方法是在三角形内部插入两个新顶点断开中间边工作量不大但效果显著。烧制变色做起来更直接顶点色 g 通道作为釉料权重在 Shader 里用 lerp 混合三种颜色——生泥色、干泥色、烧制后的陶红色。Shader 参数通过 MaterialPropertyBlock 按对象设置不同器物可以有自己的烧制色调。我习惯把湿度变化速率绑定到工具交互上被捏过的区域湿度下降得更快这样用户能直观看到「处理过的地方先干裂」的因果关系。这套方案和完全基于物理的 FEM 弹性体模拟当然是两码事但它在 Unity 里跑得动、手感可控、扩展路径清晰。如果是做教学演示或设计预览这个精度完全够用如果要做高度写实的物理仿真再考虑引入 GPU 上的质点弹簧系统也不迟前期顶点形变积累的网格数据可以无缝迁移过去。我在做这类项目时最深的体会是先让一个参数跑通全流程再谈扩展压感、对称形变、湿度这些功能一旦各个击破拼起来就是完整的作品。希望帮到你。本文还有配套的精品资源点击获取
返回列表