Unity博物馆互动项目实战:手把手教你用C#代码动态生成可塑形陶罐模型
Unity博物馆互动项目实战从零构建可塑形陶罐模型系统在数字文旅快速发展的今天博物馆互动装置正从简单的信息展示转向沉浸式体验。去年参与某省级博物馆数字化改造时我们遇到了一个有趣的需求让游客通过触控屏亲手捏制仿古陶罐同时了解陶器制作工艺。这个看似简单的需求背后隐藏着动态建模、实时形变和性能优化等一系列技术挑战。传统3D建模方式无法满足实时交互需求而现成的Asset Store插件又缺乏定制灵活性。经过多次迭代我们最终用Unity原生Mesh API开发出一套轻量级解决方案顶点数控制在200以内却能实现细腻的形变效果。本文将完整还原这个项目的技术实现路径特别适合已经掌握Unity基础但想要深入理解网格编程的中级开发者。1. 项目架构设计1.1 模块化建模思路优秀的技术方案始于清晰的模块划分。我们将陶罐分解为五个逻辑组件组件名称功能描述顶点共享策略外底罐体底部圆形平面中心点独立边缘共享外柱面罐体侧面纵向完全共享顶部环罐口边缘内外环独立内柱面罐体内侧纵向完全共享内底罐体内部底面中心点独立边缘共享这种设计带来两个关键优势性能优化共享顶点使模型总顶点数从理论值500降至实际180左右形变控制不同部位可采用不同的形变算法如外柱面侧重径向膨胀顶部侧重高度调整1.2 核心数据结构[System.Serializable] public class PotteryConfig { [Range(8, 32)] public int details 16; // 水平细分精度 [Range(3, 10)] public int layers 5; // 垂直分层数 public float radius 1.0f; // 基础半径 public float thickness 0.2f; // 罐壁厚度 public float minRadius 0.7f; // 最小收缩半径 public float maxRadius 1.5f; // 最大扩张半径 } public class DynamicPottery : MonoBehaviour { private Mesh _mesh; private ListVector3 _originalVertices; // 保存初始顶点位置 private int _seamVertexIndex; // 接缝起始索引 // ...其他运行时变量 }提示将可调参数设计为Serializable结构体便于在Inspector中实时调试同时保持代码整洁2. 网格生成核心技术2.1 外底生成算法我们从底部圆形开始构建采用中心点环形顶点的策略void CreateBottom(ListVector3 vertices, Listint triangles, ListVector2 uvs) { // 中心顶点不参与形变 vertices.Add(Vector3.zero); uvs.Add(new Vector2(0.25f, 0.25f)); float angleStep 2 * Mathf.PI / config.details; for (int i 0; i config.details; i) { float angle i * angleStep; Vector3 pos new Vector3( config.radius * Mathf.Cos(angle), 0, config.radius * Mathf.Sin(angle) ); vertices.Add(pos); // 三角形索引注意顺时针顺序 triangles.Add(0); // 中心点 triangles.Add(i 1); // 当前顶点 triangles.Add((i 1) % config.details 1); // 下一顶点 // UV映射将圆形展开为方形纹理的四分之一 uvs.Add(new Vector2( 0.25f 0.25f * Mathf.Cos(angle), 0.25f 0.25f * Mathf.Sin(angle) )); } }关键技巧顶点复用外圈顶点将被外柱面共享避免接缝UV优化将圆形映射到纹理的左上1/4区域为其他部件预留空间2.2 柱面生成优化柱面采用分层生成策略每层共享相同数量的顶点void CreateCylinder(bool isOuter, ListVector3 vertices, Listint triangles) { float currentRadius isOuter ? config.radius : (config.radius - config.thickness); int verticalSegments config.layers 1; // 包含底部和顶部 for (int y 0; y verticalSegments; y) { float height y * (config.height / config.layers); int rowStart vertices.Count; for (int x 0; x config.details; x) { float angle 2 * Mathf.PI * x / config.details; vertices.Add(new Vector3( currentRadius * Mathf.Cos(angle), height, currentRadius * Mathf.Sin(angle) )); // 生成侧面四边形两个三角形 if (y 0 x config.details) { int a rowStart x; int b rowStart x - config.details - 1; int c b 1; int d a 1; triangles.Add(a); triangles.Add(b); triangles.Add(d); triangles.Add(b); triangles.Add(c); triangles.Add(d); } } } }性能考量顶点共享纵向完全共享大幅减少顶点数量接缝处理每层多生成一个重复顶点x details确保UV正确展开内存预分配提前计算List容量避免频繁扩容3. 实时形变系统实现3.1 输入处理模块支持多输入源是博物馆项目的硬性要求我们抽象出统一的输入接口public interface IPotteryInput { bool GetTouchStart(out Vector2 screenPos); bool GetTouchMove(out Vector2 delta); bool GetTouchEnd(); } // 鼠标输入实现 public class MouseInput : IPotteryInput { public bool GetTouchStart(out Vector2 pos) { pos Input.mousePosition; return Input.GetMouseButtonDown(0); } // ...其他方法实现 } // 触摸屏输入实现 public class TouchInput : IPotteryInput { public bool GetTouchStart(out Vector2 pos) { if (Input.touchCount 1) { pos Input.GetTouch(0).position; return true; } pos Vector2.zero; return false; } // ...其他方法实现 }3.2 形变物理模型采用基于衰减半径的影响力模型使形变更符合真实陶土特性void DeformVertices(Vector3 touchWorldPos, Vector2 inputDelta) { Vector3 localTouch transform.InverseTransformPoint(touchWorldPos); float influenceRadius config.height * 0.3f; // 影响范围 Vector3[] vertices _mesh.vertices; for (int i 0; i vertices.Length; i) { if (ShouldSkipVertex(i)) continue; // 跳过中心点和接缝点 float distance Vector3.Distance( new Vector3(localTouch.x, 0, localTouch.z), new Vector3(vertices[i].x, 0, vertices[i].z) ); if (distance influenceRadius) { float attenuation 1 - (distance / influenceRadius); Vector3 deformation CalculateDeformationForce(inputDelta, attenuation); vertices[i] ApplyDeformationConstraints(vertices[i], deformation); } } _mesh.vertices vertices; OptimizeNormals(); }形变约束条件通过以下函数实现Vector3 ApplyDeformationConstraints(Vector3 vertex, Vector3 deformation) { // 径向约束 float currentRadius new Vector2(vertex.x, vertex.z).magnitude; float newRadius currentRadius deformation.x; if (newRadius config.maxRadius || newRadius config.minRadius) { deformation.x 0; } // 高度约束 float newHeight vertex.y deformation.y; if (newHeight config.height * 1.2f || newHeight 0) { deformation.y 0; } return vertex deformation; }4. 性能优化实战4.1 法线平滑策略由于接缝处存在顶点重复需要特殊处理法线计算void OptimizeNormals() { Vector3[] normals _mesh.normals; int step config.details 1; // 每层顶点数 // 处理柱面接缝 for (int i _seamVertexIndex; i normals.Length; i step) { int leftSeam i; int rightSeam i config.details; Vector3 averagedNormal (normals[leftSeam] normals[rightSeam]).normalized; normals[leftSeam] normals[rightSeam] averagedNormal; } _mesh.normals normals; }4.2 渲染优化技巧通过Shader优化提升视觉效果Shader Custom/Pottery { Properties { _MainTex (Base (RGB), 2D) white {} _RimColor (Rim Color, Color) (0.5,0.5,0.5,1) _RimPower (Rim Power, Range(0.5,8.0)) 3.0 } SubShader { Tags { RenderTypeOpaque } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 struct Input { float2 uv_MainTex; float3 viewDir; float3 worldNormal; }; sampler2D _MainTex; float4 _RimColor; float _RimPower; void surf (Input IN, inout SurfaceOutputStandard o) { o.Albedo tex2D(_MainTex, IN.uv_MainTex).rgb; // 边缘光效增强立体感 half rim 1.0 - saturate(dot(IN.viewDir, o.Normal)); o.Emission _RimColor.rgb * pow(rim, _RimPower); } ENDCG } FallBack Diffuse }5. 项目部署与扩展5.1 多平台适配博物馆环境需要特殊的部署方案触控校准void CalibrateTouchInput() { // 获取屏幕边框补偿值展柜玻璃导致的触控偏移 float borderOffset PlayerPrefs.GetFloat(BorderOffset, 0.02f); _effectiveTouchArea new Rect( borderOffset * Screen.width, borderOffset * Screen.height, Screen.width * (1 - 2*borderOffset), Screen.height * (1 - 2*borderOffset) ); }自动保存机制IEnumerator AutoSaveCoroutine() { while (true) { yield return new WaitForSeconds(300); // 每5分钟保存 SaveToJson(Path.Combine(Application.persistentDataPath, autosave.json)); } }5.2 扩展功能设计为满足后续需求我们预留了多个扩展点形状模板系统public void ApplyTemplate(PotteryTemplate template) { // 重置顶点到模板预设形状 for (int i 0; i _mesh.vertices.Length; i) { _mesh.vertices[i] template.vertices[i]; } _mesh.RecalculateNormals(); }装饰图案系统public void AddDecoration(Texture2D stamp, Vector2 uvPosition) { // 在指定UV位置叠加图案 Color[] pixels _decalTexture.GetPixels(); Color[] stampPixels stamp.GetPixels(); // ...像素混合算法 _decalTexture.Apply(); }在南京博物院实际部署时这套系统平均每帧处理时间保持在3ms以内即使在中低端设备上也能维持60fps的流畅体验。一个意外的收获是孩子们创造的陶罐形状常常超出我们预设的参数范围这促使我们增加了动态参数调整功能让系统能自动学习用户的创作风格。