简介:这是面向3D游戏与动画制作团队的角色资源制作规范文档,以《死神》角色为例,梳理从模型创建到后期优化的完整标准流程,适合3D建模师、角色美术及项目管理者参考。文档涵盖模型面数控制(普通角色1500-2000面、BOSS 3000-5000面)、UV展开与PSD选区处理、贴图规格(tga格式、512*512)与共享贴图策略,以及角色模型和贴图的拼音命名规则。后期检查部分还涉及单位设置、布线优化、多余点焊接、材质清理、坐标轴归零和光滑组设定等实操细节,能帮助团队减少沟通成本、统一交付标准。资源为1个docx文档,压缩包大小4.89MB,内容结构清晰,可直接作为内部培训或流程参考。目前已吸引109人学习,适合正在建立或优化3D角色资产管线的团队。
1. 《3D角色资源制作规范.docx》不是文档,是可执行的资产契约
一份写满了规则的角色资源规范,最后多半躺在共享盘里没人看。真正能救管线的,是让每一行规则都变成软件里跑得起来的断言,而不是只能靠人工对照的说教。外包交来的模型命名五花八门、进引擎后角色横躺或者缩放不对、贴图通道错乱导致渲染发黑——这些都不是工具差,而是规范没有落到文件、目录、坐标轴、材质路径这些能被机器检查的实体上。下面的拆解从可落地的角度出发,把3D角色资源制作规范拆成六块:单位与坐标这类硬约束、3ds Max 里的场景检查脚本、命令行与 CI 自动校验、外包验收与进引擎排错,最后落到 glTF 检查和模板化沉淀。适合正在建资源库、对接外包、搭中台的工程师,也适合想给团队立规矩但不想只靠文档的人。
2. 3D角色资源规范里的硬约束:单位、坐标、命名与目录结构
2.1 单位制和坐标轴先锁死,后面导出才不会翻车
规范文档里废话最多的三个地方通常是单位、坐标和命名,因为它们写出来只有一行,但错了影响整个管线。3ds Max 默认的单位体系是英寸,很多美术从 Maya 或 Blender 导模型进来时,没有先统一 System Unit,结果一个 170cm 的角色在引擎里变成 4 米高,或者导入时被自动缩放 25.4 倍。这类问题在验收阶段最难查,因为模型在 DCC 里看是正常的。
我一般会把单位制和坐标轴写成两条死规则:源文件统一用厘米,1 unit = 1 cm,角色原点放在脚底中心;坐标轴则按“源文件跟随 DCC 习惯、交付物跟随目标引擎”来处理。原因很简单:3ds Max、Blender、Unreal 都是 Z-up,而 Maya 和 Unity 是 Y-up,强行让所有软件用一个轴向只会让绑定和动画工具链更别扭。真正要管的是导出那一环。
| 工具 / 引擎 | 默认 Up Axis | 导入导出时的常见处理 |
|---|---|---|
| 3ds Max | Z-up | 导出 FBX 时按目标引擎设置 Up Axis |
| Maya | Y-up | 角色源文件常按 Y-up 制作 |
| Blender | Z-up | 导出 glTF 会自动转成 Y-up |
| Unity | Y-up | 导入 FBX 时可勾选坐标转换选项 |
| Unreal | Z-up | 更希望源文件直接以 Z-up 交付 |
容易忽略的点是:glTF 内部固定是 Y-up 右手系,而 FBX 内部默认也是 Y-up,3ds Max 导出时如果不显式选 Up Axis,很多版本会按场景设置写。规范里最好不要写“角色面向 Z 轴正方向”这种一刀切的话,因为每个引擎的摄像机朝向约定不同。把这条留给导出配置去管,DCC 源文件反而稳定。
2.2 根目录与文件夹层级:一条路径就能看懂角色
角色资产不是一堆散文件,而是一个有结构的目录。目录结构的作用是让脚本能找到模型、贴图、骨架和校验报告,而不是靠人肉去翻。下面是我常用的模板:
assets/characters/CH001_Elf/ ├── source/ │ ├── CH001_Elf_v01.max │ └── CH001_Elf_Tpose.fbx ├── meshes/ │ ├── CH001_Elf_LOD0.fbx │ └── CH001_Elf_LOD1.fbx ├── textures/ │ ├── CH001_Elf_BaseColor_2048.png │ ├── CH001_Elf_Normal_2048.png │ └── CH001_Elf_ORM_2048.png ├── rig/ │ └── CH001_Elf_Skeleton.fbx └── meta/ └── CH001_Elf_check.jsonsource 只放 DCC 源文件,meshes 放导出网格,textures 放最终贴图,rig 放绑定和骨架,meta 放脚本产生的校验输出。角色 ID(这里是 CH001)全局唯一,所有命名都挂在它后面,外包交付后按这个结构搬进来,脚本才能把贴图和模型自动配对。注意 source 目录不要出现 v02_final_2024 这种文件名,版本交给版本库管,文件名保持稳定。
2.3 命名规范:从文件名就能算出 LOD 和用途
命名规范最容易写成纸上谈兵。真正有用的命名,是能从文件名反推出角色 ID、平台、LOD 级别和贴图用途的。下面这个规则在 glTF 和 FBX 资产里通用:
| 片段 | 取值示例 | 说明 |
|---|---|---|
| 角色前缀 | CH_ | Character 统一前缀 |
| 角色 ID | 001 | 全局唯一,分配给每个角色 |
| 人物类型 | F / M / Cre | 女 / 男 / 生物 |
| LOD 级别 | LOD0 / LOD1 | 高模、中模、低模 |
| 部件 | Body / Head / Hair | 影响贴图与蒙皮权重区块 |
| 贴图用途 | BaseColor / Normal / ORM | 对应 PBR 材质通道 |
| 尺寸 | 2048 | 贴图宽高像素 |
整个文件名拼出来是CH_001_F_LOD0_Body_BaseColor_2048.png。配合正则可以直接让机器检查:
import re NAME_RE = re.compile( r"^CH_\d{3}_[FM]_LOD[0-3]_\w+_" r"(BaseColor|Normal|ORM)_\d{4}\.png$" ) print(bool(NAME_RE.match("CH_001_F_LOD0_Body_BaseColor_2048.png")))这个正则里\d{3}锁死角色 ID 是三位数,[FM]限定人物类型,LOD[0-3]对应四级 LOD,最后一段限定贴图通道和尺寸。写规范时把这段正直接在文档里,美术和外包自己就能预检,不用等上传后被打回。文件名统一全小写下划线,禁止空格和中文字符,这是为了避免跨平台和跨引擎的编码问题。
2.4 多边形预算分平台定:LOD 不是美术随缘,是配额
3d 建模团队最常问的问题就是三角面预算。预算不是拍脑袋的数值,它是根据目标帧率和渲染负担算出来的配额。角色模型的核心原则是:LOD0 是质量上限,LOD2 是移动端或低配机的兜底,中间按比例递减。
| 平台 | LOD0 三角面 | LOD1 | LOD2 | 单张贴图上限 |
|---|---|---|---|---|
| 移动端 | 25k - 40k | 40% | 20% | 1024 |
| 桌面端 | 60k - 120k | 50% | 20% | 2048 |
| VR / AR | 50k - 90k | 40% | 15% | 2048 |
| 影视预渲染 | 不限,需面数报告 | — | — | 4096 |
这些数值是常见经验值,具体项目可以浮动。关键是预算要写成分级的、可查的表格,而不是一句话。影视预渲染虽然不限面数,但依然要报告,否则下游的灯光和渲染合成没法估算资源。预算表最终会变成第 4 章 rules.json 里的阈值,所以从规范成立第一天起就要让数值是机器可读的。
3. 把3D角色资源规范写进3ds Max:场景检查脚本与导出参数
3.1 一个检查脚本:单位、命名、网格数量一次跑完
在 3ds Max(也就是常说的 3d max)里建规范,最省事的方式是写一个运行在场景里的检查脚本。美术不用记规则,跑一遍就知道哪里不合格。下面这段用 pymxs 写的脚本,检查三个硬性指标:系统单位是否是厘米、可渲染网格的命名是否符合正则、场景里有多少个可渲染体。
from pymxs import runtime as rt import re NAME_PATTERN = re.compile( r"^CH_\d{3}_[FM]_LOD[0-3]_\w+_[A-Za-z]+$" ) UNIT_OK = "Centimeters" def check_scene(): errors = [] unit_type = rt.Units.SystemUnitType if str(unit_type) != UNIT_OK: errors.append(f"system unit = {unit_type}, expects {UNIT_OK}") mesh_count = 0 for node in rt.rootNode.Children: if rt.isKindOf(node, rt.GeometryClass) and node.renderable: mesh_count += 1 if not NAME_PATTERN.match(node.Name): errors.append(f"bad name: {node.Name}") return errors, mesh_count if __name__ == "__main__": errors, count = check_scene() print(f"renderable meshes: {count}") for e in errors: print("[FAIL]", e)代码逻辑分三层:第一层通过rt.Units.SystemUnitType读取系统单位,第二层遍历rt.rootNode.Children顶层节点,第三层用isKindOf(node, rt.GeometryClass)配合renderable属性过滤出真正会渲染的网格。命名正则的规则和 2.3 节一致。需要注意SystemUnitType的返回值在不同语言版本的 3ds Max 里可能显示为中文,比如“厘米”,所以规范里最好做一个别名映射,把“Centimeters”、“#Centimeters”、“厘米”都视为通过,否则非英文版的美术会被误报。
3.2 UV 通道和贴图坐标:防止多余的通道残留
“3d max 删除 uv”是搜索量很高的问题,原因很现实:从高模烘焙进低模时,DCC 会自动生成一堆额外 UV 通道,导出到游戏引擎或 glTF 后通道映射就乱了。实时角色资产一般只留 UV0,如果需要 Lightmap 才允许保留 UV1,其余通道必须清理。检查脚本里可以加这样一段:
def check_uv_channels(node): num_maps = rt.meshop.getNumMaps(node.mesh) extra = [i for i in range(1, num_maps) if i != 1] if extra: return f"{node.Name}: extra uv channels {extra}" return None这里meshop.getNumMaps返回该网格的贴图通道数量,通道 1 通常是 UV0(不同 DCC 的索引习惯略有差异)。如果角色不需要 Lightmap,extra里列出的通道都应该删掉。删除时先在修改面板里把 UV 修改器塌陷,再一次性移除,不然有些版本会在塌陷时把通道重新生成回来。规范里要把这条写成白名单制:默认只有 UV0,额外通道需要负责人确认用途。这条规则特别适合放进自动检查,因为人工在 3ds Max 里看不出多余通道,只有导出后才出问题。
3.3 贴图资源规范:尺寸、色彩空间和文件名后缀
贴图规范管三件事:尺寸、色彩空间、格式。角色贴图通常有 BaseColor、Normal、ORM(AO/Roughness/Metalness)三种核心通道,后缀不一致会导致 glTF 的材质解析错位。
| 贴图 | 默认尺寸 | 色彩空间 | 后缀示例 |
|---|---|---|---|
| BaseColor | 2048 | sRGB | _BaseColor_2048.png |
| Normal | 2048 | Linear | _Normal_2048.png |
| ORM | 2048 | Linear | _ORM_2048.png |
| 细节/次表面 | 1024 | sRGB 或 Linear | _Detail_1024.png |
色彩空间写错是渲染发灰的头号原因:Normal 和 ORM 是线性数据,放进 sRGB 通道会被自动提亮。检查贴图尺寸最直接的方式是用 ImageMagick 批量扫:
identify -format "%f %wx%h %[channels]\n" \ CH001_Elf_BaseColor_2048.png正常输出是CH001_Elf_BaseColor_2048.png 2048x2048 srgba。如果 BaseColor 贴图多出 alpha 通道,而角色不需要透贴,应该导出时去掉,否则某些移动端引擎会按带透明通道的格式分配额外内存。格式层面我建议统一 PNG,避免 TGA 和 TIF 进入资产库,因为 glTF 和 Web 管线对 PNG 的支持最稳定。
3.4 导出 FBX 与 glTF:在导出面板上把规范按住
导出参数的坑比建模更隐蔽。同一份角色资源,在 3ds Max 里正常,导出到 Unity 就横躺,导出到 Unreal 就缩小,导成 glTF 进 three.js 又反转。问题几乎都出在 Up Axis 和单位换算上。我一般会在规范里附一张参数对照表:
| 导出项 | Unity 目标 | Unreal 目标 | glTF 目标 |
|---|---|---|---|
| Up Axis | Y-up | Z-up | Y-up |
| 单位基准 | Centimeters | Centimeters | Meters |
| 动画烘焙 | 有动画才勾 | 有动画才勾 | 有动画才勾 |
| 嵌入贴图 | 不嵌入 | 不嵌入 | 建议外挂 PNG |
glTF 的特殊性在于格式内部固定使用米作为单位。3ds Max 场景里角色是 170cm,导出 glTF 时如果导出器不做换算,进 three.js 后模型尺寸就会和预期差 100 倍,很多免费的 3D 人物模型 glTF 文件打开后奇大或奇小,根因就在这里。规范里要写清楚:源文件统一保持厘米,导出 glTF 前先缩放到米,或者在导出器里设置好单位换算。这条不写,后续每个用 Web 展示的环节都会踩一遍。
4. 用命令行和 CI 把3D角色资源检查跑起来
4.1 先看现状:assimp info 快速读一个 FBX
做自动校验的第一步,是不打开 3ds Max 就能看到资源的关键信息。assimp 是现在最常用的模型加载库,自带的 info 命令可以快速列出 FBX/OBJ/glTF 内部的网格数、顶点数、材质数:
assimp info assets/characters/CH001_Elf/meshes/CH001_Elf_LOD0.fbx输出里会包含类似这样的字段:Mesh count、Vertex count、Face count、Material count、Texture 列表。把 Texture 路径和你规范的目录结构一对比,缺失贴图立刻能查出来。assimp 的优势是零 DCC 依赖,任何操作系统都能跑,用来做资源入库前的快速预检非常合适。缺点是它只读几何和基本材质,读不出骨架动画细节,骨骼相关校验还是要靠 Blender headless 或专用 SDK。
4.2 写一个 Python 校验脚本:命名、贴图引用、网格数量一起查
assimp info 适合人眼看,但规范要落地必须做成程序。下面这个脚本用 pygltflib 直接读 glTF,把三角面数量、贴图是否存在、网格是否为空全部检查一遍:
import json, sys from pathlib import Path import pygltflib def load_rules(path="rules.json"): with open(path, encoding="utf-8") as f: return json.load(f) def check_gltf(filepath, rules): gltf = pygltflib.GLTF2().load(filepath) errors = [] if len(gltf.meshes) == 0: errors.append("gltf has no meshes") total_tris = 0 for mesh in gltf.meshes: for prim in mesh.primitives: if prim.indices is not None: count = gltf.accessors[prim.indices].count total_tris += count // 3 max_lod = rules["max_tris_per_lod"][0] if total_tris > max_lod: errors.append(f"tris {total_tris} > {max_lod}") for img in gltf.images: uri = img.uri or "" if not uri.startswith("data:"): p = Path(filepath).parent / uri if not p.exists(): errors.append(f"missing texture: {uri}") return errors, total_tris if __name__ == "__main__": errors, tris = check_gltf(sys.argv[1], load_rules()) print(f"tris={tris}") for e in errors: print("[FAIL]", e)prim.indices是索引缓冲,accessors[prim.indices].count是索引总数,除以 3 就是三角形数量。gltf.images里uri如果是相对路径,就按当前文件所在目录去查找贴图;如果是以data:开头,说明贴图内嵌在 glb 里,不参与外部文件检查。脚本不直接硬编码预算,而是从 rules.json 里读取,这是因为预算会随项目迭代调整,改成配置文件后,改预算不需要动代码。
rules.json 长这样:
{ "max_tris_per_lod": [80000, 32000, 16000, 6000], "texture_sizes": [1024, 2048], "naming_prefix": "CH_" }把阈值从代码里拆出来还有一个好处:美术在评审时改了预算,git 历史里能看到每次调整的记录和理由。规范不再是静态文档,而是一份跟随项目演进的配置文件。
4.3 把校验挂进 Git 钩子或 CI
脚本写好后,把它挂进 CI 是最顺理成章的事。GitLab CI 配置可以写成:
asset-check: stage: test script: - pip install --quiet pygltflib - python scripts/check_asset.py --rules rules.json --input assets/characters artifacts: paths: - check_report.json expire_in: 1 week这样做的好处是角色资产在和主线合并之前,就会被机器拦住不合格的提交。美术不需要记得跑脚本,CI 会自动把[FAIL]信息反馈到合并请求里。对于还在用 Perforce 或者 SVN 的团队,常见做法是在服务端加 pre-commit 触发器,或者用定时任务扫描共享目录。触发器的维护成本比 CI 高,但思路一样:让机器最先看见问题。
5. 3D角色资源验收实务:外包交付、扫描资产与进引擎排错
5.1 一张可直接发给外包的验收清单
规范最终要面对外包,所以验收清单必须能直接发出去,并且让外包照着逐条自查。表里的每一条都要有明确通过标准,不给“差不多”留余地:
| 检查项 | 通过标准 |
|---|---|
| 打包目录 | 严格按资产目录模板,不允许散文件 |
| 模型命名 | 匹配命名正则 |
| 单位 | DCC 内 1 unit = 1 cm,角色原点在脚底 |
| 三角面 | 不超过预算,LOD0-2 齐全 |
| UV 通道 | 只保留 UV0(或白名单内 UV1) |
| 贴图 | 全部 PNG,尺寸和色彩空间符合规范 |
| 材质 | 命名与部件一致,不出现 Material_001 |
| 骨架 | 骨骼前缀统一,根骨骼唯一 |
| T-Pose | 交付 T-Pose 绑定文件,动画单独发送 |
这条清单本身就是规范文档的简化版,外包在交付前跑一遍,能省掉至少一轮返工。需要注意的是,清单里不要写“模型质量要好”这种主观词,每一项都要落到可检查的数值和命名上。
5.2 骨架和 T-Pose:绑定交付的硬指标
骨架是角色资产里最容易被忽略的部分,因为它在 DCC 里看不到明显问题,进引擎后动画一播放就全暴露了。通常要锁死三条:根骨骼唯一,命名统一前缀,T-Pose 作为绑定基准。移动端项目里骨骼数量最好控制在 60 根以内,带手指的角色可以放宽到 80-100 根,但必须写明上限。
T-Pose 和 A-Pose 的选择一直是争议点,但规范里必须选定一个。T-Pose 的双臂水平,做动画匹配和IK解算更稳定;A-Pose 适合蒙皮初期,但对动画环节不友好。常见做法是要求外包交付 T-Pose 的绑定文件,动画文件另外导出,并且导出时不要勾选 “Bake Animation”,否则绑定姿势会被烘焙进骨骼层级,后续动画拼接会出现整体位移。
5.3 结构光扫描与点云资产怎么并入规范
角色资产不一定都从建模开始,用 3d 结构光相机或激光扫描真实演员也是常见路径。扫描出来的原始数据先是 3d 点云,要进动画管线,必须经过一套固定流程:第一,去除飞点和孤立簇,这些噪声会让后续重拓扑变得极不可控;第二,抽稀到接近目标面数的点云密度;第三,重拓扑成四边面,把高模细节烘焙到 Normal 贴图;第四,清理非流形边和自交面,模型必须水密。
点云本身通常不进最终资产库,最终资产必须有明确的拓扑、UV 和骨骼。另外,现在流行的 3D 高斯泼溅,也就是 3D Gaussian Splatting,用来做展厅级预览效果很好,但它不是动画资产,不能进角色管线的绑定和动画环节,规范里要单独把它划分为另一种交付物。这条边界不划清,后续接手的绑定师会非常痛苦。
5.4 进引擎后“坏资源”的快速定位
角色进引擎后出现的问题,绝大多数集中在四类现象。第一,材质变紫或变粉,这是贴图路径断了,跑一遍 assimp info 看 Texture 路径就能定位;第二,角色横躺,Up Axis 不对,重新指定导入轴向;第三,脸黑或法线发暗,Normal 贴图被当成 sRGB 导入,改了色彩空间就恢复;第四,动画播放后人物整体飘移,多半是根骨骼或 T-Pose 没有对齐。
如果团队里没有 3ds Max 授权,可以用 Blender headless 模式抽贴图信息:
blender -b scene.blend -P dump_textures.py在 dump_textures.py 里遍历bpy.data.images,把贴图名和文件路径打印到 stdout。这条命令的好处是可以在没有 GUI 的服务器上跑,适合把资产检查集成到 CI 里。诊断结果应该回流到规范文档的“常见问题”章节,外包在交付前就能自己跑一遍,问题不再只靠人肉抽查。
6. 把规范沉淀成角色模板:glTF 检查与运行时侧验证
6.1 用 gltf-transform 做导出后的二次检查
从 3ds Max 导出的 glb 文件,在进资源库之前,我习惯再用 gltf-transform 做一次独立检查。它不依赖 DCC 环境,直接分析 glTF 二进制里面的网格、材质和纹理信息:
npx gltf-transform inspect assets/CH001_Elf_LOD0.glb npx gltf-transform optimize \ assets/CH001_Elf_LOD0.glb \ assets/CH001_Elf_LOD0.opt.glb \ --compress draco \ --texture-compress webpinspect 的输出会列出 glTF 里的网格数、accessor 数量、材质数量和纹理列表,正好对应规范里的各类阈值。optimize 的--compress draco把顶点数据压缩,--texture-compress webp进一步减小贴图体积,适合移动端和 Web 端。需要注意,不同版本的 gltf-transform 参数写法略有差异,具体以npx gltf-transform --help为准。Normal 贴图尽量不要用有损压缩压得太狠,否则光影过渡会出现明显的块状瑕疵。
6.2 在 three.js 的 GLTFLoader 里挂一层健康检查
资源进了引擎不代表万事大吉。在 three.js 的加载回调里加一道检查,能把材质缺失这类问题在运行时立刻暴露出来:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; const loader = new GLTFLoader(); loader.load('/assets/CH001_Elf_LOD0.glb', (gltf) => { const meshes = gltf.scene.children.filter((c) => c.isMesh); if (meshes.length === 0) { console.warn('[asset-check] no meshes in gltf'); } const noMat = meshes.filter((m) => !m.material); if (noMat.length) { console.warn(`[asset-check] ${noMat.length} mesh with missing material`); } console.log(`[asset-check] meshes=${meshes.length} anims=${gltf.animations.length}`); });这段代码检查两个点:场景里有没有空网格,以及网格是否缺少材质。gltf.animations.length输出动画数量,方便确认导出时动画是否完整。这类运行时日志比美术人工看渲染结果要快得多,尤其适合在自动化的页面冒烟测试里收集资产健康度。
6.3 把“规范模板”放进共享目录,新角色一落地就校验
规范最终级的落地方式,是让新角色目录一创建就自动进入检查流程。常见做法是在资产共享目录上挂一个目录监听,用 inotifywait 监控新文件落盘:
inotifywait -m -r -e moved_to assets/characters \ --format '%w%f' | while read file; do python scripts/check_asset.py --input "$(dirname "$file")" done-e moved_to监听新文件被移动或复制进来的事件,--format '%w%f'输出完整路径,脚本收到路径后自动对所在角色目录执行校验,结果追加写入meta/check_report.md。这样从外包交付到资产入库,中间不再需要人工催着跑脚本,每个角色目录里的报告文件本身就是验收凭证。规范文档里写的每一条规则,到这里才算真正变成了管线的一部分。
本文还有配套的精品资源,点击获取