ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Unity换装Mesh合并:从Draw Call到骨骼重映射的完整方案

Unity换装Mesh合并:从Draw Call到骨骼重映射的完整方案 简介一份面向Unity开发者的角色换装技术示例工程围绕网格合并、材质管理与骨骼动画三条主线演示如何通过CombineMeshes将身体、头部、衣物等部位合并为统一网格并利用材质实例与Animator控制器实现服装切换和动作跟随。内容覆盖模型拆分、材质替换、动画重定向等关键操作适合正在学习Unity渲染优化或角色定制系统的初中级开发者。压缩包共258个文件主体包含fbx角色模型、prefab预制体、mat材质、cs换装脚本及对应meta/asset配置另含场景与工程设置整体仅4.54MB结构紧凑便于直接导入查看。已有858人学习下载。通过阅读工程中的脚本注释与对象组织方式可快速理解绘制调用优化、骨骼绑定注意事项以及换装状态的动态管理逻辑并在此基础上扩展出背包换装、外观预览等更高阶功能。1. unity mesh合并换装先认清它是渲染优化方案还是骨骼绑定方案做一个带换装的角色最常见的做法是给每个部件单独挂一个 SkinnedMeshRenderer。头发、头、上衣、下装、手套、鞋子六个部件六个材质球、六份骨骼绑定、六次 Draw Call。角色一多移动端直接烫手。把多个部件的 Mesh 合并成一个同时保留骨骼蒙皮信息就是 unity mesh合并换装要做的事。它不是把模型原地拼起来那么简单背后是骨骼索引重映射、bindposes 重建、材质与贴图合并三件事。这篇文章写给做捏脸、时装、角色预制的客户端程序看完能自己实现一套可运行的运行时合并流程也明白哪些坑绕不开。2. 为什么换装必须合并Draw Call、骨骼与蒙皮权重的关系2.1 换装系统的渲染压力六件套就是六次 Draw Call换装游戏的渲染压力比普通角色多一倍不止。一个不换装的角色往往只有一个 SkinnedMeshRenderer一张贴图、一个材质球移动端上就是一个 Batch。换装角色每个部件单独渲染六个部件就是六个 Batch如果每个部件还带描边材质、阴影材质Batch 数量直接翻倍。Unity 的静态合批对 SkinnedMeshRenderer 无效动态合批又要求网格顶点数和材质一致换装部件基本不满足条件。最终压力全部落在 CPU 的渲染状态切换和 GPU 的顶点输入上。我自己做过一次实验一个 5 万面角色拆成六个部件每个部件 8000 到 12000 面。每个部件独立渲染时Frame Debugger 里看到 6 个 Batch、12 个 SetPassCall因为有的部件带了两层材质。角色同屏 20 个Batch 冲到 120 以上中端安卓机帧率直接掉到 30 以下。合并成一个 SkinnedMeshRenderer 之后同屏 20 个角色的 Batch 数降到 20 左右帧率恢复 60。这个对比就是换装合并最直接的价值不是省显存是省状态切换。2.2 三种常见合并方案离线合并、运行时合并与骨骼换装行业内做换装合并有三条路线适用场景完全不同。离线合并也叫编辑器合并美术在导出阶段就把所有换装组合烘焙成独立 Mesh 和材质。好处是运行时零开销坏处是组合数量爆炸6 个槽位每个 10 个选项就是 10 的 6 次方种组合全部烘焙不现实。实际项目里只烘焙常用组合和默认形象。运行时合并是大多数换装项目的选择也就是把各个部件的 SkinnedMeshRenderer 在角色创建或换装瞬间合并成一个。Unity 官方的 Mesh.CombineMeshes 能合并顶点和三角形但不会处理蒙皮骨骼索引和 bindposes直接用来合并 SkinnedMeshRenderer 会导致动画播放时权重错乱。后面第 3 章的核心代码就是解决这件事。第三种叫骨骼换装不合并 Mesh只替换骨骼上挂的节点。它常用于部件差异特别大的场景比如武器、翅膀、尾巴这种独立附着物。骨骼换装的问题是部件还是独立渲染Draw Call 没有本质改善只解决了骨骼复用问题。对时装类换装Mesh 合并才是正解。2.3 合并的本质不是顶点搬家而是骨骼索引重映射很多第一次做换装合并的程序员拿着 Mesh.CombineMeshes 直接干合出来的模型静止时看不出问题动画一播就整个扭曲。原因很简单CombineMeshes 只管顶点的 position、normal、uv、triangle它不知道每个顶点受哪根骨骼影响。SkinnedMeshRenderer 的每个顶点都有 BoneWeight里面存了四根骨骼的索引和权重。这四个索引指向的是 SkinnedMeshRenderer.bones 数组而 bones 数组里每根骨骼又对应一个 bindpose也就是骨骼绑定到 Mesh 空间的逆矩阵。三个数据必须一一对应顶点权重里的索引、bones 数组里 Transform 的位置、bindposes 数组里矩阵的位置。换装部件的 bones 数组长度和顺序各不相同直接合并后索引指向错位动画自然崩。所以合并换装的核心技术动作是先把所有部件的骨骼收集成一个统一的骨骼列表再把每个顶点权重里的骨骼索引改写成统一列表里的新索引最后重建 bindposes 数组。这个流程有个严格前提所有部件必须共享同一套骨骼层级和命名。美术导出时统一 T-Pose、统一骨骼命名、统一模型空间原点运行时才能合并这点后面还会反复提到。3. 用 SkinnedMeshCombiner 在运行时合并换装核心代码与接入流程3.1 换装数据的组织槽位、部件网格与骨骼约定做换装合并先要把数据组织想清楚代码是后面的事。我一般用一个枚举定义槽位每个部件 prefab 上挂一个标记组件里面记录它属于哪个槽位、是否参与合并、是否带独立动画。部件网格在美术导出时就要满足三个约定共享同一套骨骼层级、顶点坐标在同一个模型空间、绑定姿势统一为 T-Pose 且部件 prefab 的本地变换为零。第三个约定最关键但最容易忽略很多换装部件在 prefab 里被调整过位置和旋转合并时如果不处理 transform顶点坐标和骨骼空间就对不上。如果美术给的是带偏移的部件合并前要么在资源导入阶段把网格顶点烘焙到 root 空间要么在运行时合并时用矩阵把变换消掉。我的建议是前者导入阶段用 AssetPostprocessor 或编辑器脚本做网格顶点重烘焙运行时一律按零变换处理。部件上还要注意清理冗余骨骼。很多美术导出时会把辅助挂点、空节点一并导出成骨骼节点这些节点不参与蒙皮但会进入 bones 数组导致合并后骨骼列表里出现一堆 null 或无用引用。下面的合并代码会把空引用跳过但最省事的还是导出阶段就清理掉。3.2 核心合并代码骨骼映射、bindposes 与 CombineMeshes直接给可运行的 C# 代码。这个类做了四件事收集全局骨骼列表、重映射部件网格的 BoneWeight 索引、重建 bindposes、用 CombineMeshes 合并顶点数据并重建 SkinnedMeshRenderer。using System.Collections.Generic; using UnityEngine; public static class SkinnedMeshCombiner { // 入口把 parts 里的所有部件合并进 root 上的 SkinnedMeshRenderer public static SkinnedMeshRenderer Combine( GameObject root, ListSkinnedMeshRenderer parts, string newMeshName Combined_Mesh) { // 阶段一遍历所有部件把骨骼按出现顺序收集成全局骨骼列表 ListTransform finalBones new ListTransform(); DictionaryTransform, int boneIndexMap new DictionaryTransform, int(); foreach (SkinnedMeshRenderer part in parts) { if (part null || part.sharedMesh null) continue; foreach (Transform bone in part.bones) { if (bone null) continue; // 跳过空骨骼避免索引错位 if (!boneIndexMap.ContainsKey(bone)) { boneIndexMap[bone] finalBones.Count; finalBones.Add(bone); } } } // 阶段二准备 bindposes 容器并标记哪个位置已填充 Matrix4x4[] mergedBindposes new Matrix4x4[finalBones.Count]; bool[] bindposeFilled new bool[finalBones.Count]; for (int i 0; i mergedBindposes.Length; i) { mergedBindposes[i] Matrix4x4.identity; } ListCombineInstance combineInstances new ListCombineInstance(); ListMaterial finalMaterials new ListMaterial(); foreach (SkinnedMeshRenderer part in parts) { if (part null || part.sharedMesh null) continue; // 阶段三把部件自身的 bindposes 填到全局列表对应位置 Matrix4x4[] partBindposes part.sharedMesh.bindposes; Transform[] partBones part.bones; for (int i 0; i partBones.Length; i) { Transform bone partBones[i]; if (bone null || !boneIndexMap.TryGetValue(bone, out int newIdx)) continue; if (!bindposeFilled[newIdx]) { mergedBindposes[newIdx] partBindposes[i]; bindposeFilled[newIdx] true; } } // 阶段四复制网格重映射 BoneWeight 的四个骨骼索引 Mesh partMesh Object.Instantiate(part.sharedMesh); partMesh.name part.sharedMesh.name _prepared; BoneWeight[] weights partMesh.boneWeights; for (int wi 0; wi weights.Length; wi) { BoneWeight w weights[wi]; w.boneIndex0 RemapBoneIndex(w.boneIndex0, partBones, boneIndexMap); w.boneIndex1 RemapBoneIndex(w.boneIndex1, partBones, boneIndexMap); w.boneIndex2 RemapBoneIndex(w.boneIndex2, partBones, boneIndexMap); w.boneIndex3 RemapBoneIndex(w.boneIndex3, partBones, boneIndexMap); weights[wi] w; } partMesh.boneWeights weights; // 阶段五部件本地变换必须为单位变换合并时直接用 identity if (part.transform.localPosition ! Vector3.zero || part.transform.localRotation ! Quaternion.identity || part.transform.localScale ! Vector3.one) { Debug.LogWarning(${part.name} 本地变换不是单位变换合并结果可能异常); } CombineInstance ci new CombineInstance(); ci.mesh partMesh; ci.subMeshIndex 0; // 每个部件只取第一个 submesh ci.transform Matrix4x4.identity; // 前提所有部件在同一模型空间 combineInstances.Add(ci); // 材质按部件顺序收集后续再决定是否合并贴图 Material mat part.sharedMaterials.Length 0 ? part.sharedMaterials[0] : null; finalMaterials.Add(mat); } // 阶段六合并顶点与三角形数据 Mesh finalMesh new Mesh(); finalMesh.name newMeshName; finalMesh.CombineMeshes(combineInstances.ToArray(), true, false); // 阶段七关键一步——合并后的网格必须手动覆盖 bindposes finalMesh.bindposes mergedBindposes; // 阶段八重建或复用 SkinnedMeshRenderer SkinnedMeshRenderer result root.GetComponentSkinnedMeshRenderer(); if (result null) result root.AddComponentSkinnedMeshRenderer(); result.sharedMesh finalMesh; result.rootBone root.transform; result.bones finalBones.ToArray(); result.sharedMaterials finalMaterials.ToArray(); result.updateWhenOffscreen true; return result; } // 把旧骨骼索引映射到全局骨骼列表的新索引 private static int RemapBoneIndex( int oldIndex, Transform[] partBones, DictionaryTransform, int boneIndexMap) { if (oldIndex 0 || oldIndex partBones.Length) return 0; Transform bone partBones[oldIndex]; if (bone null) return 0; if (boneIndexMap.TryGetValue(bone, out int newIndex)) return newIndex; return 0; } }代码逻辑说明阶段一是整个合并的地基它把所有部件用到的骨骼去重收进一个全局数组并用 Dictionary 记录每个 Transform 对应的新索引。阶段三把每个部件的 bindposes 按照新索引填进全局数组同一个骨骼在多个部件里出现时只填第一次的值因为同一根骨骼的 bindpose 在共享骨架下应该是一致的。阶段四对每个顶点权重调用 RemapBoneIndex把旧索引翻译成新索引。阶段六传的mergeSubMeshes: true很关键它告诉 CombineMeshes 把所有三角形的 submesh 标记统一成 0避免合并后出现一堆无意义的 submesh。useMatrices: false表示不用 CombineInstance.transform 去变换顶点这建立在上文强调的“所有部件共享同一模型空间”前提上。阶段七是运行时合并蒙皮网格最容易漏的一步。CombineMeshes 合并出的网格自带一组默认 bindposes它是根据合并矩阵生成的对蒙皮网格来说是错的。必须手动赋值我们在阶段三里拼好的 mergedBindposes动画系统才会按正确的骨骼逆矩阵计算顶点位置。参数说明里要留意几个点RemapBoneIndex 对查不到的骨骼一律返回 0也就是默认由全局骨骼列表第一根骨头兜底这能防止越界但会产生错误权重所以部件里出现未知骨骼时最好在开发期直接报错而不是静默兜底合并后 bones 数组长度等于 finalBones.CountsharedMaterials 数量等于部件数量如果你的换装还有武器这类需要独立蒙皮的部件不要把它加进 parts 列表。3.3 把合并接进换装流程换掉槽位后重新合并合并代码本身只解决“怎么合”换装流程还需要一个调用方。我的习惯是写一个 OutfitChanger 组件挂在角色根节点上保存当前所有部件的引用换装时先找到对应槽位替换再重新调用 Combine。public class OutfitChanger : MonoBehaviour { public ListSkinnedMeshRenderer currentParts new ListSkinnedMeshRenderer(); // 换掉指定槽位的部件后重新合并 public void ChangePart(string slotName, SkinnedMeshRenderer newPart) { for (int i 0; i currentParts.Count; i) { if (currentParts[i].name.Contains(slotName)) { currentParts[i] newPart; break; } } SkinnedMeshCombiner.Combine(gameObject, currentParts, Player_Combined); } }这里有个容易忽略的问题每次重新合并都会 Instantiate 一份部件网格旧网格不会自动释放。连续换装几次Mesh 对象就泄漏了。我一般把 Combine 返回的 sharedMesh 记录在角色上下次合并前手动 Object.Destroy 掉。不要在换装瞬间做合并尤其是移动端CombineMeshes 不是免费的后面避坑章会细说。4. 换装合并避坑5 个翻车现场与排查方法4.1 动画一播手指就扭成麻花BoneWeight 索引没有重映射现象合并后的模型静止时完全正常一播动画手指、脚趾、裙摆这些权重密集的区域扭曲成麻花身体其他部位看起来又没事。原因静止时顶点位置本身就是建模时的坐标不需要骨骼计算也正确。动画驱动后蒙皮阶段要按 BoneWeight 里记录的骨骼索引去 bones 数组取变换矩阵索引指向错误的骨骼权重就作用在错误关节上。手指这类关节多、权重分配细的区域最先暴露问题。解决逐顶点重映射 BoneWeight 索引这是合并代码里不能跳过的阶段四。注意只改 Mesh.boneWeights 还不够如果用了 GPU 蒙皮或者顶点数超过 65535 走了 BoneWeight4 通道BlendWeight 和 BlendIndices 也要一并重映射否则高端机型上用的还是老的索引数据。4.2 Draw Call 没降反升每个部件成了独立 submesh现象按教程合完后Frame Debugger 里 Batch 数压根没降甚至比原来还多了一两个。原因CombineMeshes 的 mergeSubMeshes 传了 false或者部件的 sharedMaterials 长度超过 1。合并时每个 submesh 对应一个材质槽材质决定渲染顺序和状态切换。六个部件即使顶点合在一起只要 submesh 没合并SkinnedMeshRenderer 还是会按六个 submesh 渲染六次。解决合并时 mergeSubMeshes 传 true且合并前把部件材质统一成一个。如果你确实需要多材质比如脸和身体用不同 shader那就要接受多个 submesh 的现状但要让相同材质的 submesh 尽量连续排列减少渲染状态切换。把六个 Draw Call 优化到两个而不是一个也算达成目的。4.3 换装瞬间卡顿掉帧CombineMeshes 在主线线程上干活现象换装按钮点下去角色卡了一两百毫秒连续换装还会越来越卡。原因CombineMeshes 是同步的 CPU 密集操作顶点越多越慢。换装瞬间要做 Instantiate 网格、重映射权重、合并顶点、重新上传 GPU 数据全部堆在渲染线程前。连续换装卡顿加剧通常是 Mesh 泄漏旧的临时网格没有被释放内存和显存持续上涨GC 频繁触发。解决换装合并不要在点击瞬间做。常见做法是换装界面打开时预合一组常用搭配真正换装时把合并拆到两到三帧里或者接受一次性卡顿但严格控制临时 Mesh 的释放。我做项目时会在 OutfitChanger 里维护一个 oldMesh 引用下一次合并前主动 Destroy并用 ObjectPool 缓存 CombineInstance 数组减少 GC 压力。4.4 空骨骼引用不报错美术导出留下的隐形炸弹现象合并时没有任何报错编辑器里看 bones 数组有一两个 null角色动画部分失效或整体偏移有些部位飘在空中。原因SkinnedMeshRenderer.bones 数组允许存在 null 引用运行时不会崩但蒙皮计算时把空骨骼当单位矩阵处理权重对应的顶点就停在原地或被错误拉扯。美术建模软件里删除骨骼操作不干净留下了冗余骨骼节点导出时带了出来。解决合并代码里对 null 骨骼做跳过处理只能治标真正要做的是在资源导入阶段校验。用一个编辑器脚本遍历所有换装部件检查 bones 数组是否有 null、是否有不在骨骼层级里的孤立节点发现问题直接拦截。开发期让美术修比运行时兜底靠谱得多。4.5 阴影和描边全乱材质模板不统一让合并白做现象合并后主体渲染正常但阴影、描边、轮廓光这些后处理效果全乱了有的部件有描边有的没有阴影颜色深浅不一。原因换装合并把顶点数据统一了材质没有统一。部件各自带材质半透明材质和不透明材质混在同一个 SkinnedMeshRenderer 里渲染管线按 submesh 逐个处理透明度排序和描边 Pass 就乱了。解决合并前定义一套统一的材质模板所有部件只换贴图不换 shader。半透明部件比如薄纱、纱裙单独挑出来不参与主 Mesh 合并作为独立 SkinnedMeshRenderer 额外渲染。双面材质和 shader 里的描边 Pass 也要在合并前确认一致换装合并优化的前提是渲染状态统一材质分叉越多合并收益越低。5. 材质与贴图合并让换装合并真正降下 Draw Call5.1 先合材质还是先合贴图顺序决定了合并上限Mesh 合并把顶点整合了材质如果不合Draw Call 只能降到部件数量这个级别。一个六个部件的角色合并后还有六个材质球Batch 还是六个。要把 Batch 压到一两个必须把材质和贴图同步合并。材质合并的做法是所有部件使用同一个 shader 模板合并时按纹理用途创建一张大的 Texture Atlas把部件各自的 albedo、normal、mask 贴图打包进去再生成一个 Material 实例。运行时合并的目标是让 sharedMaterials 的长度变成 1 或 2否则 Mesh 合并就做了一半。先合并材质还是先合贴图没有绝对顺序我的习惯是先把部件材质统一到同一个 shader 变体再处理贴图。如果 shader 不一致比如一个用 URP Lit一个用 Built-in StandardTexture Atlas 做得再整齐也没用渲染状态还是切来切去。Shader 模板统一后贴图合并才有意义。5.2 Texture Atlas 的关键参数尺寸、留白与打包顺序Texture Atlas 直接在运行时用 Texture2D.PackTextures 打包最省事。PackTextures 有三个参数会影响合并质量。一是图集尺寸。常见设置是 2048 或 4096 的二次幂尺寸。2048 适合中低端机型每张图集的像素预算大约算一下六个部件各一张 1024 贴图打包成 2048 图集就能放下前提是部件共享比例相近。部件里有超高精度贴图比如脸和头发用 2048 单张贴图时强行打包进 2048 图集要么糊掉要么浪费大量空白直接上 4096 但要注意移动端显存带宽。二是留白 padding。PackTextures 的 padding 参数控制相邻贴图之间的距离。建议设 4 像素或 8 像素防止 mipmap 采样时贴图边缘颜色互相渗透。数值太小远处看角色会有轻微色斑太大浪费图集空间。三是打包顺序。PackTextures 会自动排布矩形但顺序不稳定同一张贴图集每次打包结果可能不一样。这会影响序列帧动画里 UV 稳定性和网络同步尤其是你那套换装系统还做了翻页预览。我一般自己写一个排序规则按贴图高度降序排列后传入保证稳定的打包布局。参数推荐值说明atlasSize2048 / 4096二次幂按部件贴图数量和精度决定padding4 或 8 像素防 mipmap 边缘颜色渗透贴图格式ASTC 6x6 或 8x8移动端推荐兼顾画质与带宽mipmap开启换装角色距离变化大关闭会出现闪烁噪点打包顺序高度降序保证多次打包结果一致PackTextures 生成图集后还要把每张贴图对应的 UV 换算进图集坐标这个换算过程要在合并 Mesh 前重写 uv2 或直接改写 uv 数据。我做过最常翻车的地方就在这一步UV 坐标系是左下角还是左上角写错一次整张贴图在模型上就是上下颠倒的。5.3 Shader 模板统一半透明、双面与阴影的合并边界合并换装追求的是渲染状态统一但不是所有部件都适合合进同一个材质球。半透明材质和全透明材质是两条路不能混在一个 Atlas 里。角色的薄纱裙、蕾丝边这类部件单独用一个透明材质球和贴图不参与主 Mesh 合并否则合完后透明部分和不透明部分的深度排序一塌糊涂。双面材质的处理也要想清楚。很多时装部件是双面渲染的裙摆内侧、飘带如果把它们合并进不透明单面材质内侧会直接看不见。常见做法是双面材质部件单独出一个小 Mesh用带 Cull Off 的 shader 渲染不参与主合并。你如果用了 Unity 的双面材质 shader注意它的 renderQueue 通常要比不透明材质高合并时混在一起排序很麻烦。阴影也有类似的边界问题。换装合并后角色的 shadow caster 只有一个 Mesh如果部件里有带特殊阴影需求的比如武器要投影但不受影要么在 shader 里做阴影变体控制要么把特殊部件拆出主合并。合并换装不是把所有东西都塞进一个 SkinnedMeshRenderer 才叫完成而是把渲染批次压缩到业务允许的最小值。6. 合并结果怎么验Profiler 抓批次数与权重抽检6.1 用 Frame Debugger 确认批次数真的降了合并做完第一件事不是看模型正不正常而是打开 Window Analysis Frame Debugger找到角色渲染那一帧数一下角色占用了多少个 Batch。看两个指标角色渲染占用的 DrawCall 数量、SetPassCall 数量。如果合并前六七个 Batch合并后还是一个甚至两个渲染侧就算成功了。如果 Batch 数没降优先检查 sharedMaterials 数组长度。长度是 1 但 Batch 还是多个看 shader 的 pass 数量和 renderQueue某个 pass 用了不同的队列比如透明度队列渲染状态就会分裂。6.2 骨骼权重抽检脚本在运行时检查异常顶点动画扭成麻花的问题不一定会立刻暴露需要一个简单的抽检脚本。选几个已知位置的骨骼比如左手小指末端输出它影响的顶点数量和权重分布跟合并前对比。// 挂到角色上用来抽检某个骨骼对合并后网格的影响 public class WeightCheck : MonoBehaviour { public Transform targetBone; [ContextMenu(Check Weight)] void CheckWeight() { SkinnedMeshRenderer smr GetComponentSkinnedMeshRenderer(); int boneIndex System.Array.IndexOf(smr.bones, targetBone); if (boneIndex 0) { Debug.LogError(目标骨骼不在 bones 数组中); return; } int affected 0; float totalWeight 0f; BoneWeight[] weights smr.sharedMesh.boneWeights; for (int wi 0; wi weights.Length; wi) { BoneWeight w weights[wi]; if (w.boneIndex0 boneIndex || w.boneIndex1 boneIndex || w.boneIndex2 boneIndex || w.boneIndex3 boneIndex) { affected; totalWeight w.weight0 w.weight1 w.weight2 w.weight3; } } Debug.Log($骨骼 {targetBone.name} 影响 {affected} 个顶点总权重 {totalWeight:F2}); } }这段代码的目的是验证索引重映射是否生效抽检骨骼在合并后的 bones 数组里能找到且权重索引正确。如果 affected 是 0说明骨骼没参与蒙皮如果总权重远大于顶点数正常情况每个顶点权重合计约 1说明权重重复累计合并逻辑有 bug。合并换装做多了我养成的习惯是先立骨骼约定再动合并代码。美术那边骨骼命名不统一运行时代码写得再严谨也会在某次更新后突然崩坏。还有每次发布前跑一遍批量换装压测用同一个角色连续换装五十次看内存曲线有没有持续上涨这一条能拦住绝大多数 Mesh 泄漏问题。合并换装这套方案本身不复杂复杂的是数据规范和边界条件这两样抓牢了后面出问题的概率会小很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表