模型
引擎画 mesh,而它只加载一种 mesh 格式:.esmesh。.gltf / .glb / .fbx 是源文件,
不是资源——把它带进项目会跑一次导入,写出场景真正能引用的那些资源。
两种格式产出同样的产物、走同样的设置;FBX 独有的部分在下面单独一节。
本页讲的就是这次导入。画出结果的那个组件见组件参考; 材质整体见材质。
有七个形状不需要导入、也不需要文件。Create → Cube(以及 Sphere、Capsule、Cylinder、
Cone、Plane、Quad)建出的实体,其 MeshRenderer.mesh 里放的是一个 builtin: 引用;同样这七个
也排在任何 MeshRenderer.mesh 字段选择器的最前面。
| 引用 | 形状 |
|---|---|
builtin:cube |
100 单位的正方体,每个面一个平法线 |
builtin:sphere |
100 单位的球,平滑法线 |
builtin:capsule |
高 100 单位 —— 半径 25、两端胶囊之间 50 的圆柱,正是 CapsuleCollider3D 描述的形状 |
builtin:cylinder |
100 单位的竖直圆柱,平顶平底 |
builtin:cone |
100 单位的圆锥,立在底面上 |
builtin:plane |
XZ 平面上 100 单位的方块,朝上 —— 一块地面 |
builtin:quad |
XY 平面上 100 单位的方块,正对相机 —— 一块背板 |
它们在加载时由代码生成,所以在包里不占任何字节,引用了它的场景也不因此多出依赖。七个都
和新建的 ShapeRenderer 一样大,所以往场景里放一个不用先缩放就看得见;要多大,通过
Transform 给,和导入的模型一样。
五个封闭形状建出来时是不透明 + 背面剔除的——这才让它们互相遮挡,而不是按提交顺序混合;
plane 和 quad 是平的,所以保持双面,从背后看也还在。
图元不只是宽 100 单位,厚也是 100 单位,而按平面内容摆的相机离得不够远:沿用 2D 的默认 位置,每个实体的前半截都落在近平面之前。像 3D 示例那样,把场景相机放到几百单位之外。
导入一个模型
Section titled “导入一个模型”三个入口,同一次导入:
| 方式 | 会发生什么 |
|---|---|
把 .gltf/.glb/.fbx 拖进内容浏览器 |
源文件(连同它引用的文件)被拷进项目并导入。 |
| 直接放进项目目录 | 编辑器会发现并导入它 —— git checkout、访达里拷贝、Blender 重新导出,都算。 |
estella import-model <file> [outDir] --project <dir> |
命令行里的同一次导入。--scale <n> 决定大小。 |
产物落在源文件旁边,而不是内容浏览器当前显示的目录:重新导入必须覆盖同一批文件。
导入写出什么
Section titled “导入写出什么”以带两个 primitive、一张内嵌图的 robot.gltf 为例:
| 产物 | 是什么 |
|---|---|
robot_0_0.esmesh、robot_1_0.esmesh |
每个 primitive 一个几何文件,命名 <stem>_<mesh>_<primitive>(只有一个时就是 <stem>)。 |
robot_0.png |
文件内嵌的图片。已经在磁盘上的图片只被引用,不会复制。 |
robot_m0.esmaterial |
仅当源材质说了 MeshRenderer 装不下的东西时才写 —— 见材质。 |
robot_Walk.estimeline |
源里每个动画一个,命名 <stem>_<动画名> —— 见动画。 |
robot.esprefab |
各部分怎么组装:节点层级,每个 MeshRenderer 带上自己的 mesh、贴图、tint 和开关。 |
把 prefab 拖进场景。单个 .esmesh 也是资源(MeshRenderer.mesh 字段可以直接引用),
但 prefab 才是作者组装好的那个模型。
模型以米为单位——以厘米创作的 FBX 会在进来的路上换算好。一个世界单位是一个设计像素。 所以一个 1.7 米高的角色进来只有 1.7 个单位高 —— 几个像素 —— 除非你另行指定。
导入器不猜这个系数。模型进来很小的时候它会说出来,而修正它的那个数字就是源文件导入设置里的
Scale(在内容浏览器里选中源文件)。保存它会重新导入这个模型。
Scale 落在 prefab 根节点的 Transform 上,不会烘进几何 —— .esmesh 保持与源一致,
而那个数字始终看得见、改得动。
Lightmap UV
Section titled “Lightmap UV”烘焙好的 lightmap 是通过第二套 UV 读的,而且这套 UV 必须保证没有两个面落在同一批纹素上。 美术包裹用的那套不保证:两条手臂故意共用一条手臂的像素,这正是纹理能做小的原因。 让它们共用 lightmap 纹素,一条手臂的阴影就会落到另一条上。
在模型的导入设置里打开 Generate Lightmap UVs,导入就会展开一套:曲面被切成一片片近似平坦的 chart,每片投影到自己的平面,再按同一个世界-纹素比打包进单位方块——于是一面墙和它连着的地板 被以同样的分辨率点亮。落在 chart 边界上的顶点会被分裂,所以网格回来时顶点比源文件多; 但没有任何东西移动,每个形状、蒙皮权重和顶点色都跟着自己的顶点走。
默认关闭,因为大多数模型永远不会被烘焙,而分裂出的顶点无论有没有人读那条通道都要占内存。 有两种模型永远拿不到:
- 蒙皮网格,因为骨骼在动它,烘焙跟不上。
- 已经带第二套 UV 的模型,因为那套布局是作者的,美术可能已经按它画好了。 Blender 的 Smart UV Project 展到第二个 UV map 就是这件事,而且通常比导入器推断得更好。
导入会报告 chart 填满了图集的多少。数字偏低说明 chart 又多又小:把 chart 角度调大它们会合并, 代价是每片内部的拉伸变大。
读取端见烘焙光照。
以下情况会重新跑一次导入:
- 源文件变了(重新导出覆盖了旧的);
- 在内容浏览器里对它选择 Reimport;
- 保存了一项导入设置。
资源 uuid 会保留,所以引用这些产物的场景和 prefab 继续有效。
一个 glTF 材质按「谁装得下」拆开。
落在 MeshRenderer 组件上 —— base color,因为引擎的 mesh 通路本来就是
texture(uv) × 顶点色 × tint:
| glTF | 组件字段 |
|---|---|
baseColorTexture |
texture |
baseColorFactor |
color |
alphaMode 不是 BLEND |
opaque |
doubleSided: false |
cullBackfaces |
带 NORMAL 属性 |
lit: true |
落在 .esmaterial 里 —— 着色,那是逐材质的常量和采样器,组件没有地方放。
只在源文件用到它们时才写,基于内建的 Model shader:
| glTF | 材质参数 |
|---|---|
normalTexture |
u_normalMap |
emissiveFactor / emissiveTexture |
u_emissive / u_emissiveMap |
occlusionTexture(含 strength) |
u_occlusionMap / u_occlusionStrength |
alphaMode: MASK + alphaCutoff |
u_alphaCutoff |
metallicFactor / roughnessFactor / metallicRoughnessTexture |
u_metallic / u_roughness / u_metallicRoughnessMap |
金属度与粗糙度即使等于引擎自己的默认值也会写出来:glTF 把两个系数都默认成 1(完全粗糙的 金属),而 shader 把金属度默认成 0 —— 省略它们的产物会描述出一个源文件没有说过的表面。
材质还会把这次 draw 的混合、深度和剔除重述一遍 —— 因为材质会替换它们, 这样一个被要求自我遮挡的模型,在有了材质之后仍然自我遮挡。
源里的每个动画会在 mesh 旁边写出一个 .estimeline,prefab 的根挂一个 TimelinePlayer
指向第一个,且 playing 是关的 —— 播哪一个是场景的决定。打开 playing,或调用 timeline
API,它就开始播。
一条 clip 驱动的是它当初针对的那些节点,按名字路径在 prefab 根之下寻址。同一父节点下 重名的兄弟节点会在导入时被改成不同的名字,好让一条轨道不会驱动错对象;prefab 里显示的就是 那个名字。
LINEAR、STEP、CUBICSPLINE 都会带过来,含切线。
绑定好的模型会带着绑定进来:mesh 携带它的骨骼索引、权重和绑定姿态,prefab 给实体挂一个
MeshSkin,按那些矩阵的顺序列出骨骼实体。移动骨骼 —— 手动,或者由上面的动画驱动 ——
mesh 就会变形。
蒙皮 mesh 自己的 transform 会被忽略,这是 glTF 的要求:它的骨骼已经摆在世界里了, 所以移动 mesh 实体不会有任何效果。要移动的是骨架的根,或者骨骼本身。
| 限制 | 为什么 |
|---|---|
| 每个 mesh 最多 64 根骨骼 | 姿态走 uniform 块,64 根是 4KB —— 在 WebGL2 对一个块的保证范围内。超过的 mesh 按不变形绘制。 |
| 骨骼数必须与绑定姿态一致 | MeshSkin 列出的骨骼数对不上时按不变形绘制,而不是让顶点去解析错误的矩阵。 |
Morph target
Section titled “Morph target”模型带着可混合的形状进来(在多数 DCC 里叫 blend shape):.esmesh 在顶点旁边存下每个
target 对每个顶点的 delta,prefab 给实体挂一个 MeshMorph,它的 weights 说明当前朝
每个形状混合了多少。第 i 项对应第 i 个 target,顺序就是文件列出的顺序;名字跟着几何
走,所以权重是按位置寻址形状的。
权重 0 是作者创作时的形状,1 是完整的 target。两者都不是上界:glTF 没有规定边界,超过 1 或低于 0 都是作者可能想要的形状。
| 限制 | 为什么 |
|---|---|
| 同时最多混合 8 个形状 | 限制的是活跃数量,不是 mesh 能携带的数量——一张脸同时张开的表情就那么几个,而它的 mesh 可以有上百个形状。取权重最大的那些,其余按 0 读,这和作者没动过的权重是同一件事。 |
| 只有几何自带法线时法线才变形 | 没有 NORMAL 通道的 mesh 按一个常量法线着色,任何 delta 都弯不动它。 |
| 不带切线 | 切线基是逐像素从变形后的表面推出来的。 |
| FBX 的 in-between 形状 | 一个通道途经多个形状时只带最后一个;导入时会说明丢掉了哪些。 |
FBX 由编译进编辑器的 ufbx 读取——不需要 Autodesk FBX SDK,也没有任何东西要装。二进制与 ASCII、从 5000 系列往上的每个版本,都走同一个读取器。
FBX 需要而 glTF 不需要的那些事,导入在下游看到之前就已经了结:
| FBX 说 | 进来的是 |
|---|---|
| 它自己的 up 轴与单位(厘米、Z 朝上……) | 换算成引擎的:+Y 朝上,一个单位一米。 |
| 任意边数的多边形 | 三角形。 |
| 几何相对节点有偏移,却没有节点承载它 | 一个承载该偏移的子实体,名叫 GeometryTransform。 |
| 一个 mesh 分给多个材质 | 每段材质一个 .esmesh——正是 glTF 叫做 primitive 的那种切分。 |
| 旋转是绕 pivot 的欧拉角,还带 pre/post rotation | 烘焙成位置/旋转/缩放关键帧(曲线本来就不是线性的地方按 30 fps 重采样)。 |
材质走 ufbx 自己的映射,所以 Phong、Lambert、Maya 的 Standard Surface、3ds Max 的 Physical、Blender 的 Principled 最终都落到材质那一节列的同一组参数上——Phong 材质的粗糙度由它的高光指数换算而来,而它的金属度会被报告为没有,而不是编一个非金属表面出来。
为 FBX 创作之前,有两件关于它材质的事值得先知道:
- 金属度与粗糙度贴图只有在是同一张图时才带得过来,且要按 glTF 的方式打包(粗糙度在绿
通道、金属度在蓝通道)——引擎的 Model shader 采样的就是这一张。导出器通常写成两张独立的
图;那种情况导入会说出来,并改用常量。要么把它们打包成一张,要么在
.esmaterial上手工指定。 - 不透明度贴图不会带过来。 alpha 来自 base color 图自己的通道。
压缩几何(glTF)
Section titled “压缩几何(glTF)”EXT_meshopt_compression 与 KHR_draco_mesh_compression 在导入时解码,所以压缩过的导出件
产出与未压缩件相同的 .esmesh。运行时什么都不变:压缩是源文件的属性,不是产物的属性。
只报告、不导入的东西
Section titled “只报告、不导入的东西”导入器不会无声丢弃任何东西 —— 它带不过来的,都会在导入时作为提示出现。
| 不导入 | 为什么 |
|---|---|
weights 动画通道 |
形状本身会被导入(见 Morph target);驱动它们的剪辑还没有被带过来。 |
第二套 UV(TEXCOORD_1)、KHR_texture_transform、FBX 的 uv transform |
UV 按作者写的原样使用。 |
非三角形 primitive(mode ≠ 4) |
渲染器画的是三角形列表。 |
normalTexture.scale |
法线贴图按全强度应用。 |
| FBX 里的相机与灯光 | 场景的相机与灯光属于场景,不属于模型。 |
| FBX 分离的金属度/粗糙度贴图、FBX 的不透明度贴图 | 见 FBX。 |
值得知道的限制
Section titled “值得知道的限制”- 点光的距离在 XY 平面上算(2D 光照模型固有);方向光是精确的,而那通常正是着色一个模型需要的。
- 自己写了 vertex stage 的材质不会被重定向到 mesh 几何上 —— 那样它会无视 per-object transform。 只写 fragment 的材质两边都能用。
- 没有
NORMAL属性的几何仍然会受光,落在 2D 表面那个常量法线上 —— 平,但不是黑的。