简介:这是一份基于Unity引擎开发的陶艺制作模拟软件工程源码,面向具备一定Unity基础、希望深入理解动态网格建模技术的开发者与图形学爱好者。项目核心围绕动态生成模型、实时调整模型顶点以及动态平滑法线展开,可用于陶艺交互模拟、程序化建模等场景的学习与二次开发。压缩包共43个文件,约56KB,包含20个asset资源文件、11个meta元数据、2个mat材质、2个cs脚本以及unity场景、json配置、csproj工程等,覆盖场景、材质、脚本与工程配置等模块,结构完整可直接导入Unity运行。目前已有2500人学习下载。通过该资源,读者可掌握动态Mesh顶点变形与法线平滑的实现思路,理解陶艺造型随交互实时变化的完整逻辑,并参考配套文章进行对照学习,适合作为动态建模与Unity图形编程的实践案例。
1. 从一团泥到一只碗:Unity陶艺制作模拟软件到底在模拟什么
很多人第一次听到「Unity陶艺制作模拟软件」,脑子里浮现的是拉坯机转盘上那团泥被手指慢慢推成碗的画面。但真正动手做过的人会告诉你,难点从来不在「看起来像」,而在「手感对」。玩家手指在屏幕上滑动的速度、力度、角度,要实时映射成泥料的形变,还要保证帧率不掉、网格不炸、内存不飙——这才是这类模拟软件真正的技术核心。它适合两类人:一类是想做非遗数字化、工艺教学演示的开发者;另一类是想练手 Unity 网格变形、Shader 渲染和物理交互的进阶学习者。你不需要先成为陶艺大师,但需要理解泥料在旋转中的受力逻辑,否则做出来的东西只是「会转的圆柱体」,不是陶艺。
2. 泥料形变怎么算:从旋转体参数化到顶点位移
2.1 为什么不用刚体物理,而用参数化旋转体
陶艺拉坯的本质是「旋转对称形变」。泥料在转盘上高速旋转时,任意一个水平截面近似圆形,半径随高度变化。这意味着你不需要用刚体碰撞去模拟每一粒泥,而是可以用一条二维轮廓线绕 Y 轴旋转生成三维网格。常见做法是维护一个float[] profile数组,存储从底部到顶部每个高度对应的半径,再通过Mesh的顶点重建来更新形状。
这样做的好处很直接:顶点数可控、形变计算量小、不会出现物理引擎常见的抖动和穿透。代价是失去了一些非对称细节,比如手指按压留下的局部凹陷。对于教学演示和轻度模拟,这个取舍是值得的。如果你非要模拟非对称形变,那就得引入基于顶点的弹簧质点模型,计算量会上升一个数量级,移动端基本扛不住。
2.2 用 C# 重建旋转体网格的最小代码
下面这段代码展示了一个最简化的旋转体网格生成器。它接收轮廓数组和分段数,生成对应的顶点、法线和三角形索引。
using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class PotteryMeshBuilder : MonoBehaviour { [Range(8, 64)] public int radialSegments = 32; // 圆周分段数,越高越圆滑但顶点越多 public float[] profile = new float[] { 0.3f, 0.35f, 0.4f, 0.38f, 0.3f }; // 从底到顶的半径 public float height = 1.0f; // 总高度 private Mesh mesh; void Start() { mesh = new Mesh(); mesh.name = "PotteryMesh"; GetComponent<MeshFilter>().mesh = mesh; Rebuild(); } public void Rebuild() { int rings = profile.Length; int vertCount = rings * (radialSegments + 1); Vector3[] vertices = new Vector3[vertCount]; Vector2[] uvs = new Vector2[vertCount]; int[] triangles = new int[(rings - 1) * radialSegments * 6]; float ringStep = height / (rings - 1); for (int r = 0; r < rings; r++) { float y = r * ringStep; float radius = profile[r]; for (int s = 0; s <= radialSegments; s++) { float angle = (float)s / radialSegments * Mathf.PI * 2f; int idx = r * (radialSegments + 1) + s; vertices[idx] = new Vector3(Mathf.Cos(angle) * radius, y, Mathf.Sin(angle) * radius); uvs[idx] = new Vector2((float)s / radialSegments, (float)r / (rings - 1)); } } int t = 0; for (int r = 0; r < rings - 1; r++) { for (int s = 0; s < radialSegments; s++) { int a = r * (radialSegments + 1) + s; int b = a + radialSegments + 1; triangles[t++] = a; triangles[t++] = b; triangles[t++] = a + 1; triangles[t++] = a + 1; triangles[t++] = b; triangles[t++] = b + 1; } } mesh.Clear(); mesh.vertices = vertices; mesh.uv = uvs; mesh.triangles = triangles; mesh.RecalculateNormals(); mesh.RecalculateBounds(); } }逻辑说明:profile数组决定每一圈的高度和半径,radialSegments决定圆周方向的光滑度。每次手指交互修改profile中的某个值后调用Rebuild(),网格就会实时更新。参数方面,radialSegments建议在 24 到 48 之间,低于 24 会看出明显棱角,高于 48 在移动端会带来不必要的顶点开销。profile的长度决定了纵向精细度,一般 8 到 16 段足够表现碗、瓶、盘的基本形态。
注意:每次
Rebuild()都会重新分配数组,频繁调用会产生 GC 压力。实际项目中建议预分配缓冲区,只更新变化的部分。
2.3 手指交互怎么映射成轮廓变化
有了网格重建能力,下一步是把触摸或鼠标拖拽映射成profile的修改。常见做法是:射线从摄像机穿过手指位置,打到泥料表面,获取命中点的高度和当前半径,然后根据拖拽方向增减对应高度的半径值。
public float sensitivity = 0.01f; // 拖拽灵敏度,太大容易形变过猛 void Update() { if (Input.GetMouseButton(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { float localY = transform.InverseTransformPoint(hit.point).y; int index = Mathf.RoundToInt(localY / height * (profile.Length - 1)); index = Mathf.Clamp(index, 0, profile.Length - 1); float delta = Input.GetAxis("Mouse Y") * sensitivity; profile[index] = Mathf.Max(0.05f, profile[index] + delta); Rebuild(); } } }这里sensitivity是关键参数。设得太小,用户觉得「推不动」;设得太大,轻轻一划泥料就飞出去了。我一般会把它和当前半径挂钩,半径越大灵敏度越低,模拟泥料越多越难推动的感觉。另外Mathf.Max(0.05f, ...)是防止半径变成零或负数导致网格翻转。
3. 让泥料看起来像泥:Shader 与材质的关键参数
3.1 陶泥的视觉特征与常见翻车点
泥料在湿润状态下有微弱的高光、表面有细密颗粒感、颜色偏灰褐。很多人直接用 Standard Shader 加一张贴图,结果看起来像塑料。问题出在光滑度(Smoothness)给太高,金属度(Metallic)没归零。陶泥的 Smoothness 一般在 0.15 到 0.3 之间,Metallic 必须为 0。如果需要更真实的效果,可以用自定义 Shader 叠加一层噪声法线,模拟手指划过留下的纹理。
另一个常见翻车点是 UV 展开。旋转体网格的 UV 是规则矩形,贴图直接铺上去会出现接缝。解决办法是在圆周方向使用重复采样,或者在 Shader 里用世界坐标做三平面映射。对于陶艺模拟,接缝通常出现在背面,影响不大,但如果你要做特写展示,就得处理。
3.2 用 Shader Graph 快速搭一个陶泥材质
如果你用的是 URP 或 HDRP,Shader Graph 是最快的路径。核心节点链路是:基础颜色乘一个噪声节点,输出到 Albedo;噪声的另一个通道经过 Remap 后输出到 Smoothness;法线方向叠加一个细节法线贴图。参数上,噪声的 Tiling 建议设为 8 到 16,太小会看出重复图案,太大在移动端会闪烁。
// 简化的表面着色器片段,展示核心计算 half4 frag(v2f i) : SV_Target { half3 baseColor = _ClayColor.rgb; half noise = tex2D(_NoiseTex, i.uv * _NoiseTiling).r; half smoothness = lerp(0.15, 0.3, noise); half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv * _NormalTiling)); // 后续光照计算... }_NoiseTiling控制颗粒密度,_ClayColor建议用偏灰的褐色而不是纯棕。如果泥料看起来太「干」,把 Smoothness 整体上调 0.05 左右;如果太「湿」,就往下调。
3.3 光照与环境反射的取舍
陶艺模拟不需要复杂的光照。一盏主方向光加一个柔和的环境光就够。关键是环境反射不能太强,否则泥料表面会出现不自然的镜面反射。在 Lighting 设置里把 Environment Reflections 的 Intensity 调到 0.3 以下,或者直接用纯色环境。如果要做展示级效果,可以加一个反射探针,但记得把 Resolution 降到 64 或 128,不然包体会变大。
4. 避坑与排查:陶艺模拟开发中最容易翻车的 5 个点
4.1 网格顶点数暴涨导致帧率骤降
现象:一开始运行流畅,拉坯几分钟后帧率从 60 掉到 20 以下。原因:每次Rebuild()都重新分配顶点数组,旧数组等待 GC 回收,短时间内产生大量内存碎片。解决:预分配Vector3[]和int[]缓冲区,用mesh.SetVertices()和mesh.SetTriangles()替代直接赋值,并且只在轮廓真正变化时才重建。
4.2 手指拖拽方向与形变方向相反
现象:用户往上滑,泥料反而变矮。原因:射线命中的局部坐标 Y 轴方向与屏幕 Y 轴方向不一致,常见于摄像机有旋转角度的情况。解决:不要直接用Input.GetAxis("Mouse Y"),而是把拖拽向量转换到泥料的局部空间再取分量。或者用hit.point与上一帧命中点的差值来计算。
4.3 泥料底部出现破洞或翻转
现象:拉坯到很薄的时候,底部网格出现黑色破洞。原因:profile中某个半径值被减到接近零,相邻两圈的顶点几乎重合,法线计算出现零向量。解决:给半径设一个下限,比如 0.03,并且在RecalculateNormals()之后检查是否有 NaN 法线,有就跳过该帧更新。
4.4 Shader 在移动端显示为紫红色
现象:编辑器里正常,打包到手机后泥料变成紫红色。原因:Shader 没有适配移动端渲染管线,或者 URP 的 Shader 变体被剥离。解决:确认 Shader 的 Render Pipeline 设置正确,在 Graphics Settings 里把 Always Included Shaders 加上你的陶泥 Shader,或者用 Shader Variant Collection 显式包含。
4.5 触摸多点操作时泥料跳动
现象:两根手指同时操作时,泥料形状突然跳变。原因:Input.GetMouseButton(0)在移动端会映射到第一根手指,但第二根手指的射线检测也会触发Rebuild(),两个修改叠加导致轮廓突变。解决:用Input.touchCount判断,只取第一根手指的Touch数据,或者给每个手指分配独立的修改区域。
5. 进阶技巧:用 Perlin 噪声做泥料表面细节与验证方法
5.1 在轮廓上叠加低频噪声
纯手工拉坯的泥料表面不会绝对光滑,会有细微的起伏。用Mathf.PerlinNoise在半径上叠加一个低频扰动,可以让成品看起来更自然。关键参数是噪声的频率和幅度:频率太高会像砂纸,太低看不出效果。我一般用Mathf.PerlinNoise(y * 3f, 0f) * 0.02f,也就是在高度方向每单位变化 3 次采样,幅度控制在半径的 2% 以内。
float noise = Mathf.PerlinNoise(y * 3f, 0f) * 0.02f; float finalRadius = profile[r] + noise;这个扰动不需要每帧更新,可以在Rebuild()时一次性计算。注意噪声的种子要固定,否则每次重建形状都会轻微变化,用户会觉得「泥料自己在动」。
5.2 怎么验证形变逻辑是对的
一个实用的验证方法是:在编辑器里画一条目标轮廓曲线,然后让程序自动把profile从初始状态渐变到目标状态,观察中间过程是否平滑、有没有自交或翻转。如果中间某帧出现网格穿透,说明相邻半径变化太快,需要插入更多纵向分段或者限制单帧最大变化量。
另一个验证点是体积守恒。拉坯过程中泥料总量不变,所以你可以用数值积分估算当前网格的近似体积,和初始体积对比。偏差超过 15% 就说明形变逻辑有问题,通常是某些高度的半径被过度修改而相邻高度没有同步调整。
5.3 一个我踩过的坑
早期版本里我为了追求「手感」,把灵敏度设得很高,结果用户轻轻一碰泥料就飞出去,根本没法塑形。后来改成根据当前半径动态调整灵敏度,并且加了阻尼系数,手感才稳定下来。另一个教训是不要忽视移动端的 GC,每次Rebuild()都 new 数组,跑几分钟就卡顿。预分配缓冲区之后,同样的场景在中端手机上也能稳定 60 帧。如果你也在做类似的东西,先把形变和交互跑通,再花时间调 Shader 和光照,顺序反了会浪费很多时间。希望帮到你。
本文还有配套的精品资源,点击获取