跳到主要内容

加载模型

构建场景使用了基本图元,实际项目通常需要模型。本页介绍完整流程:加载 GLB、查看内部结构,再直接调整,无需打开三维制作工具。

使用的模型为 Lionsharp Studios 的 Porsche 911 Carrera 4S,采用 CC BY-SA 4.0,也是汽车配置器使用的资源。这是带有真实导出瑕疵的 Sketchfab 模型,正适合说明实际处理方法。

导出格式​

支持两种 glTF:.gltf 为 JSON,旁边另放纹理和几何体;.glb 将全部内容放在一个二进制文件。Web 推荐 GLB,只需一个请求,也避免相对路径损坏。

导出时尤其应注意以下命名,因为后续操作将依赖它们:

  • 节点名称。 <pc-node> 按名称查找部件,如果导出名为 Object_12,代码也要使用它。
  • 材质名称。 用于定位和替换材质,节点名称失去意义时,材质名称往往仍有价值。

命名不佳并非无法补救,hierarchy() 会报告实际结构,但在 Blender 中先整理名称通常更省时间。

加载与实例化​

需要两个标签:<pc-asset> 声明文件,<pc-model> 将其层级实例化到场景:

<pc-app>
<pc-asset id="car" src="assets/porsche-911-carrera-4s.glb"></pc-asset>
<pc-scene>
<pc-entity name="camera" position="3.2 0.9 3.6" rotation="-12 42 0">
<pc-camera clear-color="#dfe4ea"></pc-camera>
</pc-entity>
<pc-model asset="car"></pc-model>
</pc-scene>
</pc-app>

很多模型至此即可显示,但这辆车还需要压缩网格解码器和供车漆反射的环境,下面分别添加。

GLB 是容器资源,包含网格、材质、纹理、蒙皮和动画,<pc-model> 从容器实例化层级。<pc-asset> 根据 .glb 或 .gltf 扩展名推断 container 类型。带查询参数如 car.glb?v=2 或无扩展名的 URL 则应显式指定 type="container"。模型还像 <pc-entity> 一样接受 position、rotation 和 scale,并可嵌套到其他实体中。

内容进入场景后模型就绪并触发 load。失败同样结束就绪等待,但 contentEntity 为 null,并触发 error,文件可能无法获取时应监听:

document.querySelector('pc-model').addEventListener('error', (event) => {
console.warn(`the model did not load: ${event.message}`);
});
模型原点由制作者决定

模型轴心和缩放不会自动归一化。这辆车原点在车身中央,车轮位于 y=0 下方,直接放在原点地面上会下陷。可调整 <pc-model> 的 position 或移动地面,应逐资源处理,不能假定统一规范。

压缩网格​

模型常用 Draco 减小下载体积,本例也是如此。Draco 需要 WebAssembly 解码器,作为 <pc-app> 子项通过 <pc-wasm> 声明:

<pc-wasm name="DracoDecoderModule"
glue="modules/draco/draco.wasm.js"
wasm="modules/draco/draco.wasm.wasm"></pc-wasm>

两个文件分别为 Draco 胶水脚本和 WASM 二进制,不属于引擎 npm 包,需要自行提供。可从 Draco 发布版本获取,或复制 Web Components 示例中的文件,并配置对应地址。

<pc-app> 启动前等待全部直接子 <pc-wasm>,因此场景运行时解码器已就绪。缺失时模型加载失败,控制台会显示引擎尝试从页面旁边获取 draco.wasm.js。

同一机制也可提供 Basis 转码器,使用 KHR_texture_basisu 的模型需要它。

通过 EXT_meshopt_compression 或 KHR_meshopt_compression 使用 meshoptimizer 压缩的模型无需配置,库已内置解码器并传给引擎。

为模型提供光照​

车漆为金属,没有漫反射颜色,可见外观来自反射。只有清屏颜色而没有环境时,即使灯光很亮,车身仍接近黑色。多数 PBR 模型都需要可反射的环境,金属尤其如此。

<pc-sky> 可提供环境。指定图像并添加 lighting,场景便使用图像照明和反射。enabled="false" 隐藏背景,但继续照亮车体:

<pc-asset id="chapel" src="assets/sepulchral-chapel-rotunda-4k.webp"></pc-asset>
<!-- ... -->
<pc-scene>
<pc-sky asset="chapel" enabled="false" lighting></pc-sky>
<!-- ... -->
</pc-scene>

以下实时预览基于加载与实例化,加上 pc-wasm 和 pc-sky,资源由本站提供:

实时示例
<pc-app>
<pc-wasm name="DracoDecoderModule" glue="https://developer.playcanvas.com/assets/modules/draco/draco.wasm.js" wasm="https://developer.playcanvas.com/assets/modules/draco/draco.wasm.wasm"></pc-wasm>
<pc-asset id="car" src="https://developer.playcanvas.com/assets/porsche-911-carrera-4s.glb"></pc-asset>
<pc-asset id="chapel" src="https://developer.playcanvas.com/assets/sepulchral-chapel-rotunda-4k.webp"></pc-asset>
<pc-scene>
<pc-sky asset="chapel" enabled="false" lighting></pc-sky>
<pc-entity name="camera" position="3.2 0.9 3.6" rotation="-12 42 0">
<pc-camera clear-color="#dfe4ea"></pc-camera>
</pc-entity>
<pc-model asset="car"></pc-model>
</pc-scene>
</pc-app>

可以尝试:

  • 删除 pc-sky 的 lighting,观察车漆变暗。
  • 删除 enabled="false",查看车体反射的环境。
  • 删除 pc-wasm,汽车不再出现,控制台显示 draco.wasm.js 请求失败。

查看实际加载内容​

三维工具中的名称经常不同于最终引擎名称。导出器可能重命名,引擎还会为无名节点生成名称,为同名同级项添加后缀。

不要猜测。<pc-model> 的 hierarchy() 会报告实际树,一行即可打印:

import { whenReady } from '@playcanvas/web-components';

const model = await whenReady('pc-model');
console.log(String(model.hierarchy()));
Sketchfab_model
└─ Root
├─ window_rear
│ └─ window_rear_0 (render) {window}
├─ windshield
│ ├─ windshield_0 (render) {window}
│ └─ windshield_1 (render) {plastic}
├─ Plane.002
│ └─ Plane.002_0 (render) {paint}
├─ Plane.003
│ └─ Plane.003_0 (render) {paint}
├─ Plane.004
│ └─ Plane.004_0 (render) {paint}
├─ boot
│ └─ boot_0 (render) {full_black}
├─ underbody
│ └─ underbody_0 (render) {full_black}
├─ Cylinder.000
│ ├─ Cylinder.000_0 (render) {silver}
│ ├─ Cylinder.000_1 (render) {plastic}
│ ├─ Cylinder.000_2 (render) {rubber}
│ └─ Cylinder.000_3 (render) {Material.001}
├─ Plane
│ └─ Plane_0 (render) {Material}
⋮
├─ bumper_front.004
│ ├─ bumper_front.004_0 (render) {silver}
│ ├─ bumper_front.004_1 (render) {lights}
│ └─ bumper_front.004_2 (render) {plastic}
├─ bumper_front.007
│ └─ bumper_front.007_0 (render) {glass}
⋮
├─ boot.005
│ └─ boot.005_0 (render) {paint}
⋮
├─ boot.011
│ ├─ boot.011_0 (render) {coat}
│ └─ boot.011_01 (render) {coat}
└─ Cube.002
└─ Cube.002_0 (render) {full_black}

示例在各 ⋮ 处省略了内容,Root 实际有 32 个子项,其余均为真实输出。

每行对应一个节点,包括名称、括号中的 render 及其他组件,以及花括号中的材质。由输出可发现:

  • 节点名缺乏意义。 boot.011、Plane.002、Cylinder.000 等来自导出器;材质名 paint、glass、rubber、silver、window、lights 却有明确含义,因此本模型更适合按材质定位。
  • Render 在叶节点上。 windshield 自身没有几何体,其子项 windshield_0 才有。要替换材质,应绑定带 render 标记的节点,而非容易识别的父节点。
  • Cylinder.000 是同轴的一对车轮,其下分别有轮辋、塑料、轮胎和刹车节点。
  • boot.011_01 由引擎重命名。 原 GLB 有两个同名 boot.011_0 子项,引擎构建层级时加后缀区分。

hierarchy() 返回 name、path、index、components、materials、children 等纯数据,因此也可程序化搜索,字段详见检查层级。

// Every node that has geometry painted with the 'paint' material
const painted = [];
const walk = (node) => {
if (node.materials.some(m => m.name === 'paint')) painted.push(node.name);
node.children.forEach(walk);
};
walk(model.hierarchy());
console.log(painted); // ['Plane.002_0', 'Plane.003_0', 'Plane.004_0', ...]

调整加载的内容​

<pc-node> 绑定模型内节点并声明覆盖值。每个需修改的部件都在 pc-model 内嵌套一个。name 用于查找而非重命名,未指定属性会保留模型原始设置。

隐藏部件​

Sketchfab 模型常附带烘焙阴影平面,本例还将作者水印烘焙在其中。一个属性即可隐藏:

<pc-model asset="car">
<pc-node name="Plane" enabled="false"></pc-node>
</pc-model>

enabled="false" 禁用节点及全部后代,无需编辑文件即可移除不需要内容。替换部件材质示例已隐藏平面。

调整部件姿态​

pc-node 的 position、rotation 和 scale 替换原始变换,而非在其上叠加:

<pc-model asset="car">
<!-- Lift a front wing clear of the body, leaving its rotation and scale as exported -->
<pc-node name="boot.005_0" position="0 0 0.5"></pc-node>
</pc-model>

具体节点可查询 hierarchy()。本例 boot.005_0 使用 paint 材质,实际是前翼子板。

为什么 0 0 0.5 是抬高,而不是 0 0.5 0?变换相对父项,继承祖先旋转。根 Sketchfab_model 绕 X 轴 -90°,将 Z 向上的源模型摆正,因此内部 Z 向上、-Y 向前。很多导出模型类似,实际观察后再判断轴向。

因为是替换,运行时移除属性或将对应 JavaScript property 设为 null,会恢复原始值,适合切换两种状态。

替换部件材质​

material-overrides 将选择器映射到 <pc-material> 的 id。name: 选择器替换该节点中所有使用对应材质名称的网格实例。下面将车漆改为糖果红,同时隐藏阴影平面:

实时示例
<pc-app>
<pc-wasm name="DracoDecoderModule" glue="https://developer.playcanvas.com/assets/modules/draco/draco.wasm.js" wasm="https://developer.playcanvas.com/assets/modules/draco/draco.wasm.wasm"></pc-wasm>
<pc-asset id="car" src="https://developer.playcanvas.com/assets/porsche-911-carrera-4s.glb"></pc-asset>
<pc-asset id="chapel" src="https://developer.playcanvas.com/assets/sepulchral-chapel-rotunda-4k.webp"></pc-asset>
<pc-material id="candy-red" name="Candy Red" diffuse="#c8102e" metalness="1" roughness="0.25"></pc-material>
<pc-scene>
<pc-sky asset="chapel" enabled="false" lighting></pc-sky>
<pc-entity name="camera" position="3.2 0.9 3.6" rotation="-12 42 0">
<pc-camera clear-color="#dfe4ea"></pc-camera>
</pc-entity>
<pc-model asset="car">
<pc-node name="Plane" enabled="false"></pc-node>
<pc-node name="Plane.002_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="Plane.003_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="Plane.004_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="boot.001_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="boot.002_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="boot.005_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="boot.008_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
</pc-model>
</pc-scene>
</pc-app>

注意,每个使用车漆的节点都需要一个 pc-node。material-overrides 只作用于当前节点 Render,本例 paint 分散在七个节点,因此完整改色需要七处绑定。已知节点列表时可通过前述搜索获得,多个节点可共享同一材质声明。

可以尝试:

  • 改变 diffuse,或将 roughness 提高到 0.6,形成缎面效果。
  • 删除 boot.005_0 绑定,该前翼子板会保持原始车漆。
  • 为该绑定添加 position="0 0 0.5",同时抬高翼子板。每个节点只能绑定一次,全部覆盖属性需写在同一个 pc-node 上。

要一次遍历整个模型或在不同外观间渐变,应使用脚本,如汽车配置器。声明式方式适合预先已知的固定部件。

替代材质应设置 name,便于 hierarchy() 识别,否则显示为 Untitled。完整选择器规则,包括多材质的 index: 和无效规则报告,详见覆盖材质。

为部件挂接内容​

pc-node 可以包含 <pc-entity> 子项,它们创建在被绑定节点下,将任意节点变为继承变换的挂点:

<pc-model asset="car">
<pc-node name="bumper_front.004">
<!-- A headlight beam, dipped towards the road -->
<pc-entity position="0 -2.96 0.05" rotation="10 0 0">
<pc-light type="spot" color="#fff6e0" intensity="12" inner-cone-angle="20" outer-cone-angle="34"></pc-light>
</pc-entity>
</pc-node>
</pc-model>

子实体变换相对该节点,因此随部件运动。但本模型各节点都位于模型原点,几何体位置已烘焙,并继承根的旋转轴。因此灯光位置从车体中央测量,-Y 向前,0 -2.96 0.05 位于车灯处。灯光沿实体负 Y 照射,rotation="10 0 0" 使光束略向路面。应根据可见结果调试挂接位置。

为部件添加组件​

pc-node 接受与 pc-entity 相同的组件标签,添加到被绑定节点。常见用途是物理:网格碰撞可从节点自身 Render 取得形状,配合刚体使导出几何体具备碰撞:

<pc-model asset="car">
<pc-node name="underbody_0">
<pc-rigid-body type="static"></pc-rigid-body>
<pc-collision type="mesh"></pc-collision>
</pc-node>
</pc-model>

物理需要像 Draco 一样声明 Ammo,详见 <pc-wasm>。已有同类型组件不会重复添加,例如对已有 Render 的节点添加 pc-render 会警告且不生效。

让部件可交互​

绑定节点后它会成为拾取目标,因此 pc-node 支持指针事件,可使单个部件可点击而其余部分保持静态:

<pc-model asset="car">
<pc-node name="boot.005_0" onclick="this.setAttribute('position', '0 0 0.5')"></pc-node>
</pc-model>

内联处理器的 this 是 pc-node 元素,使用 setAttribute 可保持标记与场景一致。JavaScript property 则有类型,例如 position 与 rotation 需要 Vec3 而非字符串,因此内联处理宜用 attribute,完整脚本宜用 property。

事件及内联属性形式详见 pc-node 参考。

动画​

直接放在模型内的 <pc-anim> 播放容器动画。空标签会按轨道名添加全部动画并播放第一段,一行即可播放文件自带内容。此霸王龙包含行走循环:

实时示例
<pc-app>
<pc-asset id="t-rex" src="https://developer.playcanvas.com/assets/t-rex.glb"></pc-asset>
<pc-scene>
<pc-entity name="camera" position="3 1.1 2.4" rotation="-10 55 0">
<pc-camera clear-color="#dfe4ea"></pc-camera>
</pc-entity>
<pc-entity name="light" rotation="45 30 0">
<pc-light intensity="1.5"></pc-light>
</pc-entity>
<pc-model asset="t-rex">
<pc-anim></pc-anim>
</pc-model>
</pc-scene>
</pc-app>

无需额外包装实体,pc-model 本身就是实体宿主,内部组件直接挂到模型,和 pc-entity 相同,也能拥有名称、变换、指针处理和额外子实体。

可通过组件查看导出的动画:

const anim = await whenReady('pc-anim');
console.log(anim.clips); // ['Animation']

引擎保留片段名中的点号用于混合树路径。空 pc-anim 会跳过名称带点的轨道并警告。真实文件中很常见,例如示例机器人只有 Armature|mixamo.com|Layer0 轨道,这是 Mixamo 常见命名。

显式声明片段可绕过此问题。单轨文件可由 <pc-anim-clip> 指定自己的片段名称:

<pc-model asset="robot">
<pc-anim>
<pc-anim-clip name="walk"></pc-anim-clip>
</pc-anim>
</pc-model>

显式片段还可设置速度、循环并引入其他文件。多轨文件中,name 必须与轨道完全一致,包括大小写:

<pc-model asset="hero">
<pc-anim clip="Idle" transition-time="0.3">
<pc-anim-clip name="Idle"></pc-anim-clip>
<pc-anim-clip name="Run" speed="1.2"></pc-anim-clip>
<pc-anim-clip name="wave" asset="wave-glb" loop="false"></pc-anim-clip>
</pc-anim>
</pc-model>

示例中 Idle 和 Run 来自角色 GLB,wave 来自单轨 wave-glb 资源。修改 clip 即可切换,按 transition-time 交叉淡变:

document.querySelector('pc-anim').setAttribute('clip', 'Run');

轨道按节点名称绑定,因此跨文件动画要求节点名匹配;也因此刚性部件层级和蒙皮骨架一样可以动画。完整说明及淡变、暂停、引擎不报告片段完成等限制详见 <pc-anim>。

故障排查​

没有模型,控制台提到 Draco 或 Basis。 缺少压缩数据解码器,详见压缩网格。

没有模型,也没有警告。 检查缩放和原点。厘米单位导出可能大一百倍,原点远离几何体也会使模型完全落在视野外。

模型发黑或比三维工具中暗。 缺少反射环境,金属尤其需要。添加启用 lighting 的 pc-sky,详见模型光照。

pc-node 名称有歧义。 多个节点同名,元素不会猜测。警告列出候选,可通过 index 选择。

pc-node 名称未匹配。 两次编辑距离内的近似名称会作为建议,便于发现拼写错误。否则打印 hierarchy(),名称可能在导出时改变。

material-overrides 提示没有原始 Render。 绑定到了分组父节点,而非带几何体的叶节点,应查找 hierarchy() 中的 render 标记。

材质显示 Untitled 或 defaultGlbMaterial。 分别是未命名 glTF 材质和无材质图元的引擎默认名,不唯一,应使用 index:。-flatShaded 后缀表示引擎为无导出法线的图元创建的副本,name: 需包含后缀。

模型加载但动画不播放。 需在内部添加 <pc-anim>,详见动画。已有时检查 anim.clips 和控制台。无动画警告表示导出丢失,跳过轨道警告则可能因为名称包含点,应通过 pc-anim-clip 自定义名称。

独立文件动画不生效。 片段与模型节点名称必须一致,打印 hierarchy() 比较。

后续步骤​