放入模型与材质
接着第一个应用的空场景,往里放东西。
网格 = 图元 + 材质
场景里可见的物体是 Mesh,由两部分组成:
- 图元(primitive) —— 顶点数据,决定形状。
- 材质(material) —— 决定表面如何响应光照。
内置了几种常用图元:BoxShape、SphereShape、PlaneShape、CylinderShape、TorusShape。
// Create scene and light
const scene = new Scene();
const light = new DirectionalLight(scene);
light.lookAt(Vector3.one(), Vector3.zero(), Vector3.axisPY());
// Create a lambert material
const material = new LambertMaterial();
material.albedoColor = new Vector4(1, 0, 0, 1);
// Create a sphere mesh
new Mesh(scene, new SphereShape(), material);
// Create camera
scene.mainCamera = new PerspectiveCamera(scene, Math.PI / 3, 1, 100);
scene.mainCamera.lookAt(new Vector3(0, 0, 4), Vector3.zero(), new Vector3(0, 1, 0));
scene.mainCamera.controller = new OrbitCameraController();注意第 24–25 行加了一个方向光。大多数材质在没有光源时是全黑的,所以放物体的同时 要记得打光。
新建的网格默认在世界坐标原点,所以把相机放到 (0, 0, 4) 并 lookAt() 原点。 lookAt(eye, target, up) 定义在 SceneNode 上,相机和普通节点(包括这里的方向光)都能用。
选择材质
引擎内置了多种材质,入门阶段先认这三个:
| 材质 | 用途 |
|---|---|
UnlitMaterial | 不受光照,用于纯色标记、辅助显示、特效 |
LambertMaterial | 简单漫反射,开销低,适合风格化或性能敏感场景 |
PBRMetallicRoughnessMaterial | 基于物理的渲染,写实效果的默认选择 |
引擎还提供皮肤、头发、眼睛、卡通(MToon)等专用材质,以及用编辑器蓝图自定义的材质, 这些等熟悉基础之后再看。
加贴图
PBR 材质配上贴图才能出效果:
// Create a PBR material
const material = new PBRMetallicRoughnessMaterial();
// metallic 0.9
material.metallic = 0.9;
// roughness 0.6
material.roughness = 0.6;
// Load albedo map and normal map
getEngine()
.resourceManager.fetchTexture('https://cdn.zephyr3d.org/doc/assets/images/earthcolor.jpg')
.then((texture) => {
material.albedoTexture = /** @type {import('@zephyr3d/device').Texture2D} */ (texture);
});
getEngine()
.resourceManager.fetchTexture('https://cdn.zephyr3d.org/doc/assets/images/earthnormal.png', {
linearColorSpace: true
})
.then((texture) => {
material.normalTexture = /** @type {import('@zephyr3d/device').Texture2D} */ (texture);
});metallic 越高越像金属,roughness 越高高光越发散。
注意法线贴图传了 linearColorSpace: true,而颜色贴图没有。 这不是风格问题:颜色贴图存的是 sRGB 编码的颜色,需要转换到线性空间参与光照;而法线贴图、金属粗糙度贴图、遮罩、高度图存的是 数据而不是颜色,做 sRGB 转换会得到错误结果。漏掉这个选项通常表现为凹凸方向和强度不对。
贴图加载是异步的,fetchTexture() 返回 Promise。已加载过的资源会被缓存,同一路径不会重复请求。
加载模型
实际项目里的物体大多来自模型文件,而不是内置图元。这里有两条路径:
- 加载预制体(推荐) —— 先在编辑器里导入模型并保存为
.zprefab,运行时用instantiatePrefab()加载。 - 直接加载源模型 —— 装
@zephyr3d/loaders并注册导入器,用fetchModel()读 glTF/GLB/FBX。
推荐第一条:@zephyr3d/scene 核心不含任何模型格式的解析代码,走预制体可以不把导入器 打进产品包;预制体保存的是序列化后的引擎对象图,你在编辑器里改过的材质、节点属性、脚本 都会一并恢复。需要在运行时加载用户上传的任意模型时才用第二条。详见 资源加载与模型导入。
import { HttpFS, Vector3 } from '@zephyr3d/base';
import {
Scene,
Application,
OrbitCameraController,
PerspectiveCamera,
DirectionalLight,
getInput,
getEngine
} from '@zephyr3d/scene';
import { backendWebGL2 } from '@zephyr3d/backend-webgl';
const myApp = new Application({
backend: backendWebGL2,
canvas: document.querySelector('#my-canvas'),
runtimeOptions: {
// When using the editor workflow, the asset path must be correctly configured
VFS: new HttpFS('https://cdn.zephyr3d.org/doc/tut-10')
}
});
myApp.ready().then(function () {
// Create scene and light
const scene = new Scene();
const light = new DirectionalLight(scene);
light.lookAt(Vector3.one(), Vector3.zero(), Vector3.axisPY());
// Load a model
getEngine()
.resourceManager.instantiatePrefab(scene.rootNode, '/assets/Duck.zprefab')
.then((model) => {
model.position.setXYZ(0, -0.5, 0);
});
// Create camera
scene.mainCamera = new PerspectiveCamera(scene, Math.PI / 3, 1, 100);
scene.mainCamera.lookAt(new Vector3(0, 0, 3), Vector3.zero(), new Vector3(0, 1, 0));
scene.mainCamera.controller = new OrbitCameraController();
getInput().use(scene.mainCamera.handleEvent, scene.mainCamera);
getEngine().setRenderable(scene, 0);
myApp.run();
});加载编辑器产出的资产必须先配好 VFS(第 18–21 行)——instantiatePrefab() 的路径是 VFS 路径而不是 URL,上例用 HttpFS 把它映射到一个 HTTP 根目录。VFS 的其他实现(内存、 IndexedDB)见虚拟文件系统。
组织场景层级
节点以树形结构组织,每个节点的变换都相对于父节点。移动父节点,所有后代跟着动:
child.parent = parent;
child.position.setXYZ(0, 2, 0); // 相对 parent 向上 2 个单位这套机制以及遍历、查找、显示隐藏、包围盒等内容见场景图及节点。
下一步
- 加上阴影与后处理 —— 让画面像样起来