基于MediaPipe与Unity的实时3D虚拟试衣系统开发实践
1. 项目概述当虚拟试衣遇见实时人体捕捉最近几年虚拟试衣的概念越来越火从电商平台到线下门店大家都想给用户一个“所见即所得”的试穿体验。但说实话很多方案要么是简单的2D贴图效果生硬要么需要用户上传照片流程繁琐再或者就是依赖昂贵的深度摄像头和动捕设备成本高得吓人普通开发者和小团队根本玩不起。直到我开始研究MediaPipe和Unity的结合才感觉这条路走通了。MediaPipe是谷歌开源的一个跨平台机器学习管道框架它最厉害的地方在于用普通的RGB摄像头比如你的笔记本摄像头或手机前置镜头就能实时、高精度地检测人体姿态、手部、面部甚至全身的关节点。而Unity作为顶级的实时3D内容创作平台在渲染、物理模拟和交互逻辑上的能力毋庸置疑。把这两者结合起来意味着我们能用极低的硬件成本在Unity里构建一个能实时捕捉用户姿态、并让虚拟服装“穿”在用户身上的3D虚拟试衣间。这个项目的核心目标很明确打造一个运行在普通PC或网页端用户只需打开摄像头就能实时看到自己穿上虚拟服装的3D效果并且服装能随着身体动作自然摆动的交互式应用。它解决的不仅仅是“看”的问题更是“动”的体验——服装的物理摆动、褶皱变化、与身体的碰撞这些细节才是让虚拟试衣变得真实可信的关键。无论是想为电商应用增加卖点、为游戏开发角色定制功能还是制作新颖的AR互动营销内容这套技术栈都提供了一个极具性价比的起点。2. 核心技术栈选型与架构设计为什么是MediaPipe Unity这个选择背后是经过一番权衡的。2.1 MediaPipe轻量级、实时、跨平台的人体感知引擎市面上能做人体姿态估计的模型不少比如OpenPose、AlphaPose等。但MediaPipe有几个决定性的优势让我最终选择了它实时性MediaPipe的BlazePose模型经过高度优化在主流CPU上就能达到实时30fps的推理速度。这对于需要即时反馈的试衣体验至关重要任何卡顿都会破坏沉浸感。轻量级与易集成它提供了预训练的、开箱即用的模型并且有Python、JavaScript、C等多种语言的API。对于Unity项目我们可以通过其Python库在服务端处理视频流或者更直接地使用MediaPipe的JavaScript版本配合Unity的WebGL在浏览器端直接完成计算架构更简洁。丰富的输出BlazePose模型能输出33个3D人体关节点坐标尽管Z轴深度是相对值这包括了身体、手臂、腿部和面部关键点。这33个点足以构建一个简化但有效的人体骨骼用于驱动Unity中的角色。免训练对于大多数虚拟试衣应用MediaPipe提供的通用人体姿态模型精度已经足够。我们不需要自己收集数据、训练模型大大降低了技术门槛和项目周期。注意MediaPipe的3D坐标是“相机坐标空间”下的其原点在图像中心并且Z值表示的是关节点相对于臀部中点的相对深度并非真实的米制单位。这在后续映射到Unity世界坐标时需要特别注意和处理。2.2 Unity实时渲染与物理模拟的核心Unity在这里扮演着“舞台”和“导演”的角色实时3D渲染提供高质量的光照、材质和着色器让虚拟服装看起来逼真。物理引擎这是实现服装动态效果的核心。我们需要利用Unity的Cloth组件或更专业的布料模拟插件如Obi Cloth根据驱动骨骼的运动来模拟服装的物理行为。动画系统利用从MediaPipe获取的关节点数据驱动一个Unity内置的或自定义的骨骼模型Rig进行运动。多平台发布可以轻松打包成Windows/Mac应用、移动端APP或WebGL网页应用最大化覆盖用户场景。2.3 整体系统架构设计基于以上分析我设计了两种主流的技术实现路径各有优劣路径一本地Python服务 Unity客户端通信适合高性能PC应用Python端使用mediapipePython库打开摄像头持续进行姿态估计。数据传输将计算出的33个关节点坐标可能需要经过平滑滤波通过Socket如UDP或共享内存等方式实时发送给Unity客户端。Unity端创建一个C#脚本作为网络客户端接收数据并将其映射到一个人形角色Humanoid或通用骨骼的对应关节上驱动其运动。优点Python端处理灵活可以利用强大的Python生态进行额外的数据处理或模型切换。缺点需要用户同时运行Python脚本和Unity程序部署稍显复杂网络延迟可能带来轻微不同步。路径二纯WebGL方案MediaPipe.js Unity WebGL前端在HTML页面中通过JavaScript调用MediaPipe的JavaScript版本mediapipe/pose进行浏览器内的实时姿态估计。通信使用Unity WebGL与JavaScript的互操作jslib或SendMessage将关节点数据从JS环境传递到Unity的WebGL实例中。Unity端与路径一类似用C#脚本接收数据并驱动骨骼。优点用户只需打开一个网页无需安装任何软件体验最流畅易于传播和集成。缺点浏览器的性能限制可能比本地Python更严格Unity WebGL的构建体积和初始化速度需要优化。考虑到项目的演示和传播便利性下文将主要围绕路径二WebGL方案展开详细实现。这也是目前看来最具实用性和前景的方案。3. 开发环境搭建与核心配置工欲善其事必先利其器。这一步的坑最多配置对了后面就顺风顺水。3.1 Unity项目初始化与设置首先创建一个新的Unity项目我推荐使用Unity 2021 LTS或2022 LTS版本稳定性有保障。渲染管线选择URPUniversal Render Pipeline因为它对WebGL的支持更好且性能更优。创建项目与导入必要资源新建一个3DURP项目。我们需要一个基础的人体模型。可以从Unity Asset Store搜索“Mixamo”或“Adobe Fuse”相关的免费模型或者使用带有人形骨骼Humanoid Avatar的任意模型。确保模型的骨骼结构与MediaPipe的33个关节点大致对应如臀部、膝盖、肩膀、肘部等。导入你准备好的虚拟服装模型FBX格式。服装最好已经绑定好骨骼权重Skinned Mesh Renderer或者我们将使用Unity Cloth组件来模拟。关键项目设置Player Settings切换到WebGL平台。在Player - Resolution and Presentation中取消勾选“Fullscreen Mode”根据你的UI设计设置默认窗口大小。在Player - Publishing Settings中将Compression Format设置为Disabled。这点非常重要MediaPipe.js的WASM模块在压缩后可能无法正确加载。同样在Publishing Settings中建议将WebGL Template改为一个比较简洁的模板或者自定义模板以便于我们插入JavaScript代码。3.2 MediaPipe.js环境集成这是连接浏览器摄像头和Unity场景的桥梁。获取MediaPipe Pose库 在你的项目根目录下与Assets同级创建一个WebGLTemplates文件夹如果不存在再在里面创建一个自定义模板文件夹例如MyWebGLTemplate。将以下文件放入该文件夹index.html(主页面文件)pose_solution.js(从MediaPipe官方示例或npm包中获取)pose_solution.wasm(MediaPipe的WebAssembly二进制文件用于加速计算)编写集成的HTML/JavaScript代码 修改index.html核心是初始化Pose估计器并与Unity通信。!DOCTYPE html html langen head meta charsetutf-8 titleUnity MediaPipe Virtual Fitting Room/title script srchttps://cdn.jsdelivr.net/npm/mediapipe/pose/pose.js/script script srcpose_solution.js/script !-- 你的自定义JS -- /head body !-- Unity WebGL Canvas -- canvas idunity-canvas/canvas !-- 视频元素用于显示摄像头画面可选 -- video idinput_video styledisplay: none;/video script // Unity实例引用 var unityInstance null; // Pose对象 let pose null; // 初始化MediaPipe Pose function initPose() { pose new Pose({locateFile: (file) { // 指定WASM等资源文件的路径确保它们能被正确加载 return ./${file}; }}); pose.setOptions({ modelComplexity: 1, // 0:轻量1:完整2:重型更精确但更慢 smoothLandmarks: true, // 平滑关节点减少抖动 enableSegmentation: false, // 是否启用背景分割虚拟试衣通常需要 smoothSegmentation: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 }); pose.onResults(onPoseResults); // 设置结果回调函数 } // 当Pose计算出结果时调用 function onPoseResults(results) { if (!results.poseLandmarks) { return; // 未检测到人体 } // 将33个landmarks数据发送给Unity if (unityInstance unityInstance.SendMessage) { // 将landmarks数组扁平化为一个长的数字数组 [x1,y1,z1, x2,y2,z2, ...] const flatLandmarks []; for (const lm of results.poseLandmarks) { flatLandmarks.push(lm.x, lm.y, lm.z); } // 调用Unity中GameObject上的方法 unityInstance.SendMessage(PoseReceiver, OnPoseLandmarks, flatLandmarks.join(,)); } } // 启动摄像头并开始处理 async function startCamera() { const videoElement document.getElementById(input_video); try { const stream await navigator.mediaDevices.getUserMedia({ video: true }); videoElement.srcObject stream; videoElement.addEventListener(loadeddata, () { // 开始循环发送视频帧给Pose处理器 async function processFrame() { if (!pose) return; await pose.send({image: videoElement}); requestAnimationFrame(processFrame); } processFrame(); }); } catch (e) { console.error(Camera access failed:, e); } } // Unity加载完成后的回调需要在Unity中触发 window.unityGameLoaded function(instance) { unityInstance instance; initPose(); startCamera(); }; /script !-- Unity的加载脚本会自动插入到这里 -- /body /html在Unity中创建通信桥梁 在Unity中创建一个C#脚本PoseReceiver.cs挂载到一个空的GameObject命名为PoseReceiver上。这个脚本负责接收JS传来的数据。using UnityEngine; using System; public class PoseReceiver : MonoBehaviour { // 用于存储33个关节点每个点Vector3 private Vector3[] landmarks new Vector3[33]; // 一个公开的数组供其他脚本如骨骼驱动脚本读取 public Vector3[] Landmarks landmarks; // 由JavaScript调用的方法 public void OnPoseLandmarks(string dataStr) { if (string.IsNullOrEmpty(dataStr)) return; string[] strValues dataStr.Split(,); if (strValues.Length ! 33 * 3) // 33个点 * (x,y,z) { Debug.LogWarning($Data length mismatch. Expected {33*3}, got {strValues.Length}); return; } try { for (int i 0; i 33; i) { float x float.Parse(strValues[i * 3]); float y float.Parse(strValues[i * 3 1]); float z float.Parse(strValues[i * 3 2]); // MediaPipe坐标系转换到Unity坐标系 // MediaPipe: 原点在图像中心Y轴向下Z轴指向屏幕内相对深度 // Unity: 原点在中心Y轴向上Z轴指向屏幕外 landmarks[i] new Vector3( (x - 0.5f) * 2, // 将[0,1]映射到[-1,1] (0.5f - y) * 2, // Y轴翻转并映射 z // Z轴方向可能需要调整这里先保留原值后续根据模型缩放 ); } } catch (FormatException e) { Debug.LogError($Failed to parse landmark data: {e.Message}); } } }实操心得在WebGL构建中SendMessage是Unity与JS通信最稳定、兼容性最好的方式尽管它不是性能最高的。确保Unity中接收消息的GameObject名称‘PoseReceiver’和方法名‘OnPoseLandmarks’与JS调用中的完全一致大小写敏感。另外WASM文件加载失败是WebGL部署中最常见的问题务必确认Publishing Settings中的压缩已禁用并且WASM文件的路径在HTML中配置正确。4. 人体骨骼驱动与坐标映射拿到了33个关节点数据下一步就是让Unity里的角色“活”起来。这里的关键在于坐标空间的转换和骨骼映射。4.1 创建与配置驱动骨骼在Unity场景中我通常不会直接使用一个高精度的人体网格而是先用一个简单的、带骨骼的模型来调试驱动逻辑。准备角色模型导入一个带Humanoid Avatar的角色模型。在Inspector窗口的Rig页签下将Animation Type设置为Humanoid然后点击Configure或Apply。确保骨骼映射正确通常Unity的自动映射就很好用。创建骨骼驱动脚本新建一个C#脚本PoseDriver.cs将其挂载到角色模型的根节点上。using UnityEngine; public class PoseDriver : MonoBehaviour { public PoseReceiver poseReceiver; // 拖拽赋值 public Transform[] targetBones; // 对应MediaPipe 33个关节点的Unity骨骼Transform数组 public float positionSmoothing 5.0f; // 位置平滑系数 public float rotationSmoothing 5.0f; // 旋转平滑系数 public float globalScale 2.0f; // 全局缩放因子用于适配模型大小 private Vector3[] previousPositions; private Quaternion[] previousRotations; void Start() { if (targetBones null || targetBones.Length ! 33) { Debug.LogError(TargetBones array must be initialized with exactly 33 Transforms!); enabled false; return; } previousPositions new Vector3[33]; previousRotations new Quaternion[33]; // 初始化历史数据 for (int i 0; i 33; i) { if (targetBones[i] ! null) { previousPositions[i] targetBones[i].position; previousRotations[i] targetBones[i].rotation; } } } void Update() { if (poseReceiver null) return; Vector3[] landmarks poseReceiver.Landmarks; if (landmarks null || landmarks.Length ! 33) return; // 计算臀部中心MediaPipe索引23,24为左右髋部 Vector3 hipCenter (landmarks[23] landmarks[24]) / 2.0f; for (int i 0; i 33; i) { if (targetBones[i] null) continue; // 1. 计算目标世界位置以臀部中心为参考点并应用缩放 Vector3 targetWorldPos transform.position (landmarks[i] - hipCenter) * globalScale; // 2. 平滑位置减少抖动 Vector3 smoothedPos Vector3.Lerp(previousPositions[i], targetWorldPos, Time.deltaTime * positionSmoothing); targetBones[i].position smoothedPos; previousPositions[i] smoothedPos; // 3. 计算并平滑旋转这是一个简化版更准确的做法需要根据骨骼父子关系计算 // 例如对于上臂可以根据肩膀、肘部、手腕三个点计算旋转 if (i 11 || i 12) // 左/右肩膀 { int elbowIndex (i 11) ? 13 : 14; // 左肘13右肘14 if (targetBones[elbowIndex] ! null) { // 计算从肩膀指向肘部的方向 Vector3 toElbowDir (landmarks[elbowIndex] - landmarks[i]).normalized; // 这里需要根据你的骨骼初始朝向使用Quaternion.LookRotation或Quaternion.FromToRotation来计算目标旋转 // 示例假设骨骼初始朝向是“右”或“前” // Quaternion targetRot Quaternion.FromToRotation(Vector3.right, toElbowDir); // Quaternion smoothedRot Quaternion.Slerp(previousRotations[i], targetRot, Time.deltaTime * rotationSmoothing); // targetBones[i].rotation smoothedRot; // previousRotations[i] smoothedRot; } } // ... 为其他关键骨骼肘部、膝盖等实现类似的旋转计算 } // 4. 可选根据关键点调整根节点的位置和旋转使角色整体面向摄像头 AdjustRootPositionAndRotation(landmarks, hipCenter); } void AdjustRootPositionAndRotation(Vector3[] landmarks, Vector3 hipCenterWorld) { // 计算肩膀中心 Vector3 shoulderCenter (landmarks[11] landmarks[12]) / 2.0f; // 计算身体朝向从臀部中心指向肩膀中心在XZ平面上的投影 Vector3 bodyForward new Vector3(shoulderCenter.x, 0, shoulderCenter.z) - new Vector3(hipCenterWorld.x, 0, hipCenterWorld.z); if (bodyForward.magnitude 0.01f) { bodyForward.Normalize(); Quaternion targetBodyRot Quaternion.LookRotation(bodyForward, Vector3.up); transform.rotation Quaternion.Slerp(transform.rotation, targetBodyRot, Time.deltaTime * rotationSmoothing); } // 将臀部中心位置应用到角色根节点 Vector3 targetRootPos transform.position; targetRootPos.x hipCenterWorld.x * globalScale; targetRootPos.z hipCenterWorld.z * globalScale; // Y轴位置可以根据需要调整例如固定在地面 transform.position Vector3.Lerp(transform.position, targetRootPos, Time.deltaTime * positionSmoothing); } }手动映射骨骼在Unity编辑器中将PoseDriver脚本的targetBones数组大小设置为33。然后根据MediaPipe的关节点索引表将场景中角色对应的骨骼Transform拖拽到数组的每一个元素上。例如索引0鼻子 - 角色的鼻子骨骼索引11左肩 - 角色的左肩骨骼索引12右肩 - 角色的右肩骨骼索引23左髋 - 角色的左髋骨骼索引24右髋 - 角色的右髋骨骼... 以此类推。核心难点与技巧MediaPipe的33个点并不完全对应Unity Humanoid Avatar的所有骨骼。我们需要一个映射表并且对于没有直接对应的点如眼睛、手指可以选择忽略或者用它们来驱动更高级的面部或手部动画。最关键的映射是臀部、肩膀、肘部、膝盖、脚踝这些点决定了基本的姿态。旋转的计算比位置复杂一个实用的方法是在驱动脚本中我们主要更新骨骼的位置而依靠Unity的动画系统或逆向运动学IK来根据这些位置计算合理的旋转。例如可以使用Unity的Animator配合自定义的OnAnimatorIK回调根据MediaPipe提供的肢体端点位置来设置IK目标让Unity自动计算中间关节的旋转。4.2 坐标空间转换详解这是让虚拟角色动作和摄像头中人动作同步的灵魂步骤很多抖动、错位问题都源于此。MediaPipe坐标系归一化的屏幕空间。原点图像左上角(0, 0)。X轴向右递增范围[0, 1]。Y轴向下递增范围[0, 1]。Z轴从摄像头指向被摄体。z0大致在摄像头平面值越大表示离摄像头越远但这是相对于臀部中心的相对深度不是真实距离。Unity世界坐标系原点场景中心。X轴右。Y轴上。Z轴前屏幕外。转换公式在PoseReceiver中已部分实现unityX (mediapipeX - 0.5f) * 2 * scaleFactor;// 将X从[0,1]映射到[-1,1]再乘以一个缩放因子来控制动作幅度。unityY (0.5f - mediapipeY) * 2 * scaleFactor;// Y轴需要翻转。unityZ mediapipeZ * depthScale;// Z值需要乘以一个深度缩放系数这个系数需要根据你的场景大小和模型比例反复调试确定。有时为了简化可以先忽略Z值只使用X和Y。比例与对齐调试创建一个简单的可视化调试工具在Unity中用小球GameObject实时显示接收到的33个点转换后的位置。这样你可以直观地看到MediaPipe捕捉到的“骨架”是否和你的3D角色模型对齐。调整globalScale和depthScale参数使得当人站在摄像头前做“T-pose”时虚拟骨架和3D角色模型能大致重合。5. 虚拟服装的绑定与物理模拟驱动了人体骨骼接下来就是让衣服“穿”上去并动起来。这里有几种主流方案。5.1 方案一蒙皮网格渲染器Skinned Mesh Renderer绑定这是最传统、性能最优的方法适用于紧身或变形不剧烈的服装。准备服装模型服装模型需要预先在3D建模软件如Blender, Maya中绑定到与驱动骨骼完全相同或兼容的骨骼结构上。这意味着服装模型的骨骼权重Vertex Weights是绑定在“虚拟角色骨骼”上的。在Unity中设置将服装FBX导入Unity。确保其Rig类型也是Humanoid并且Avatar能够重定向到你的驱动角色上。将服装模型的Skinned Mesh Renderer组件中的“Root Bone”和“Bones”数组指向场景中正在被PoseDriver驱动的那些骨骼Transform。工作原理当PoseDriver脚本更新骨骼的位置和旋转时绑定在这些骨骼上的服装网格会自动跟随变形。这种方法完全依赖CPU的蒙皮计算速度快但无法模拟布料特有的飘动、碰撞等物理效果。5.2 方案二Unity Cloth组件模拟对于需要动态效果如裙子、披风、宽松上衣的服装Unity自带的Cloth组件是一个不错的选择。准备服装模型服装模型可以是一个简单的、未绑定的网格。甚至可以直接用一个面片Plane来模拟裙子。添加Cloth组件在服装模型GameObject上添加Cloth组件。配置Cloth参数Stretching Stiffness / Bending Stiffness控制布料的拉伸和弯曲刚度值越高越不易变形。Damping阻尼模拟空气阻力让运动更快停止。External Acceleration可以设置一个向下的重力加速度。World Velocity / World Acceleration Scale影响布料对角色运动的响应程度。设置碰撞体这是关键Cloth需要与角色身体碰撞体进行交互才能“穿”在身上。在驱动骨骼的关键部位如胸部、臀部、大腿添加CapsuleCollider或SphereCollider并勾选Cloth组件中“Colliders”列表下的“Capsule Colliders”或“Sphere Colliders”将这些碰撞体拖入。Cloth组件会计算网格顶点与这些碰撞体的相互作用从而实现“衣服不穿透身体”的效果。连接骨骼驱动Cloth组件有一个“Skinned Mesh Renderer”选项你可以将其指向一个由驱动骨骼控制的、不可见的“身体模型”的Skinned Mesh Renderer。这样Cloth就会跟随这个身体模型的大体运动再叠加自身的物理模拟。实操心得Unity Cloth在简单场景下效果尚可但调试非常繁琐参数之间相互影响大且容易穿模顶点穿透碰撞体。对于复杂的多层服装或高精度要求它往往力不从心。一个常见的技巧是将Cloth的“Self Collision”和“Inter-Collision”距离设置得非常小或者直接关闭因为它的自碰撞计算开销大且不稳定。5.3 方案三第三方布料插件如Obi Cloth对于商业级或要求高的虚拟试衣我强烈推荐使用专业的布料插件比如Obi Cloth。它基于粒子的物理求解器比Unity原生Cloth强大和稳定得多。导入Obi插件从Asset Store购买并导入Obi Cloth。创建Obi角色使用Obi提供的工具为你的驱动角色生成一个“Obi Actor”。这会在角色皮肤表面生成一层粒子代表碰撞表面。为你的服装模型创建一个“Obi Cloth”对象。同样Obi会为服装网格生成粒子。设置约束与碰撞在Obi Cloth上可以设置各种约束Distance Constraints保持布料形状、Bending Constraints控制弯曲、Skin Constraints这是关键——将布料粒子“绑定”到驱动角色的骨骼或Obi Actor的粒子上实现穿衣效果。将角色的Obi Actor添加到Obi Cloth的“Colliders”列表中这样布料就会与身体碰撞。驱动你仍然用PoseDriver驱动角色的骨骼。Obi Cloth通过Skin Constraints绑定到这些骨骼上骨骼运动时会通过皮肤约束拉动布料同时布料自身的物理模拟重力、风、碰撞会在此基础上产生丰富的动态细节。方案对比与选择建议特性Skinned Mesh (方案一)Unity Cloth (方案二)Obi Cloth (方案三)效果真实性低静态变形中基础动态高逼真动态性能开销低中高配置复杂度低需美术前期绑定中高抗穿模能力无完全跟随骨骼弱强适合服装类型紧身衣、内衣、T恤简单的裙子、披风所有类型尤其是复杂、多层服装对于虚拟试衣间项目如果追求效果方案三Obi Cloth是首选。如果考虑性能和快速原型可以混合使用方案一和方案二贴身衣物用蒙皮外套或裙子用Cloth。6. 性能优化与常见问题排查将实时视频流、AI推理、3D渲染和物理模拟整合在一起对性能是巨大的挑战。以下是我在项目中积累的优化和排错经验。6.1 性能优化策略MediaPipe端优化降低输入分辨率在初始化Pose时可以设置videoElement的宽度和高度或者使用pose.setOptions传入一个较低分辨率的画布。640x480的分辨率对于姿态检测通常已经足够能显著降低计算量。选择合适模型modelComplexity设置为0轻量或1完整。除非需要极高的手部或面部精度否则1是平衡点。降低帧率不一定需要每秒30帧全部处理。可以在JavaScript中用一个计数器每2帧或3帧调用一次pose.send依然能保证流畅性。Unity渲染端优化简化服装模型在保证视觉效果的前提下尽可能减少服装模型的面数。使用LODLevel of Detail系统当摄像机远时切换为低模。优化布料模拟如果使用Obi Cloth减少布料的粒子数量是最有效的优化手段。在Obi的ObiCloth组件中调整resolution参数。控制物理更新频率Unity的物理模拟包括Cloth默认每秒更新50次Fixed Timestep。对于布料模拟可以尝试适当降低这个频率如改为30在Project Settings - Time中调整Fixed Timestep。使用GPU Skinning如果使用方案一蒙皮确保在Player Settings中启用了GPU Skinning可以将蒙皮计算从CPU转移到GPU释放CPU资源。WebGL构建优化减少代码包体积使用Code Stripping在Player Settings - Publishing Settings移除未使用的引擎代码。压缩纹理对所有纹理使用合适的压缩格式如ASTC for WebGL。启用引擎代码裁剪在Player Settings - Publishing Settings - Enable Engine Code Stripping。6.2 常见问题与解决方案实录在开发过程中我遇到了无数个坑这里把最有代表性的几个列出来问题1Unity WebGL构建后摄像头无法启动或MediaPipe不工作。排查打开浏览器的开发者工具F12查看Console控制台和Network网络标签页。可能原因与解决WASM加载失败Network标签页中查看pose_solution.wasm文件是否成功加载返回200状态码。如果返回404检查文件路径如果返回0字节或错误确保Unity WebGL构建的压缩格式已禁用见3.1节。跨域问题CORS如果页面通过file://协议打开某些浏览器会限制摄像头访问和WASM加载。必须通过HTTP服务器运行比如使用VS Code的Live Server插件或python -m http.server。HTTPS要求现代浏览器中getUserMedia摄像头API在非localhost的HTTP页面上可能被阻止。开发时用localhost上线时必须使用HTTPS。问题2虚拟角色动作抖动严重不流畅。原因MediaPipe输出本身有噪声网络传输或JS-Unity通信可能有延迟或丢帧平滑处理不足。解决在MediaPipe端开启平滑pose.setOptions({ smoothLandmarks: true })。这是第一道防线。在Unity端进行滤波在PoseDriver脚本中我使用了Vector3.Lerp进行指数平滑移动。可以尝试更强的滤波算法如卡尔曼滤波器Kalman Filter或一阶低通滤波器专门针对每个关节点的位置和速度进行滤波能极大提升稳定性。降低数据更新频率与其每帧都更新不如在JS端或Unity端判断只有当姿态变化超过某个阈值时才发送数据。问题3服装穿模布料穿透身体。原因碰撞体设置不正确布料模拟步长太大物理引擎更新频率跟不上角色运动速度。解决细化碰撞体在身体关键部位布置更多、更贴合的小型碰撞体而不仅仅是用几个大的胶囊体。增加布料约束在Obi Cloth中增加Skin Constraints的约束强度让布料更紧贴身体。同时确保Collision Constraints碰撞约束的迭代次数足够。调整物理步长如果角色运动很快可以尝试减小Unity的Fixed Timestep如从0.02降到0.01让物理更新更频繁但会增大CPU负担。使用“厚度”在布料和碰撞体的设置中都有一个“厚度”Thickness或“偏移”Offset参数。适当增加碰撞体的厚度可以创建一个缓冲区域有效防止穿模。问题4角色比例失调或动作幅度不对。原因坐标映射的缩放因子globalScale,depthScale不合适骨骼映射关系错误。解决可视化调试如前所述用小球实时显示MediaPipe骨架。对比虚拟角色和真实视频中自己关节的位置关系。标定流程实现一个简单的“标定”模式。让用户站在摄像头前做一个标准姿势如双臂平伸的T-pose程序自动计算此时虚拟骨架双臂的长度与用户模型双臂长度的比例并以此动态调整globalScale。对于深度可以提示用户先后退一步、前进一步来估算深度缩放系数。问题5在低端设备上帧率很低。综合优化降级方案检测设备性能如通过Unity的SystemInfo或JS的navigator.hardwareConcurrency动态调整画质。低端设备上关闭阴影、降低渲染分辨率、使用更简单的布料模拟甚至回退到静态蒙皮。分帧处理将MediaPipe推理、Unity渲染、布料模拟等耗时操作分散到不同的帧中执行避免同一帧内卡顿。使用Web Workers将MediaPipe的JS计算部分放到Web Worker中避免阻塞主线程和Unity的渲染。这套UnityMediaPipe的虚拟试衣间方案从技术验证到效果打磨是一个不断迭代和调试的过程。它最大的魅力在于用消费级硬件打开了实时、交互式3D内容的大门。当你第一次看到屏幕里的虚拟角色实时地、流畅地模仿你的每一个动作并且身上的衣服也随之自然摆动时那种成就感是无与伦比的。虽然过程中充满了参数调试和问题排查但每一个问题的解决都让整个系统更稳健一分。