Skip to content

第一个应用

本页从空项目开始,跑出一个能看到东西的页面。假设你已经装好了包, 并且用 Vite 之类的构建工具。

HTML 骨架

引擎需要一个 canvas 元素来渲染。让它铺满窗口:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Zephyr3d app</title>
    <style>
      * { margin: 0; padding: 0; }
      html, body { width: 100vw; height: 100vh; }
      canvas { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
    </style>
  </head>
  <body>
    <canvas id="my-canvas"></canvas>
    <script type="module" src="./main.js"></script>
  </body>
</html>

canvas 的 CSS 尺寸就是渲染分辨率的依据,引擎会自动跟随它的变化,你不需要手动处理 窗口 resize。

创建应用

一个 Zephyr3d 项目有且只有一个 Application 实例。

js
// 引入Vector4
import { DEPTH_CLEAR_VALUE, Vector4 } from '@zephyr3d/base';
import { Application } from '@zephyr3d/scene';
import { backendWebGL2 } from '@zephyr3d/backend-webgl';

// Create the application
const myApp = new Application({
  // Use WebGL2 as the rendering backend
  // We currently support three types of rendering backends: WebGL, WebGL2 and WebGPU.
  backend: backendWebGL2,
  // Canvas element
  canvas: document.querySelector('#my-canvas')
});

// Wait for the rendering device to be ready
myApp.ready().then(function () {
  // Frame event handler
  myApp.on('tick', function () {
    // device is the rendering device, and we call its clearFrameBuffer method to clear the screen to green
    myApp.device.clearFrameBuffer(new Vector4(0, 1, 0, 1), DEPTH_CLEAR_VALUE, 0);
  });
  // The app has been initialized and the rendering loop begins
  myApp.run();
});

三个步骤:

  1. new Application({ backend, canvas }) —— 指定后端和画布。
  2. myApp.ready() —— 返回 Promise,渲染设备初始化完成后 resolve。所有引擎调用都要在这之后。
  3. myApp.run() —— 启动主循环。

主循环每帧触发一次 tick 事件,更新和渲染写在它的回调里。上例每帧把画面清成绿色, 所以你会看到一整片绿色。

关于 clearFrameBuffer() 的深度参数:@zephyr3d/base 导出的 DEPTH_CLEAR_VALUE 常量,不要硬编码 0 或 1。引擎支持 reverse-Z 深度约定,近平面对应的深度值会随约定改变, 这个常量在两种约定下都正确,详见深度约定

应用创建后,任何地方都可以用 getApp()getEngine()getDevice()getInput() 这几个全局函数取到实例,不必到处传递引用。

选择渲染后端

上例固定用了 WebGL2。实际项目通常希望优先使用 WebGPU,不支持时回退:

javascript
import { backendWebGL1, backendWebGL2 } from '@zephyr3d/backend-webgl';
import { backendWebGPU } from '@zephyr3d/backend-webgpu';

async function selectBackend() {
  if (await backendWebGPU.supported()) {
    return backendWebGPU;
  }
  if (await backendWebGL2.supported()) {
    return backendWebGL2;
  }
  return backendWebGL1;
}

const myApp = new Application({
  backend: await selectBackend(),
  canvas: document.querySelector('#my-canvas')
});

后端差异需要留意:WebGPU 支持计算着色器,WebGL 完全不支持;部分特性(如 ABuffer OIT、 DOM 阴影)也只在 WebGPU 上可用。引擎在能力不足时通常会静默回退到替代实现, 所以跨后端开发时要在目标设备上实测,不能只看代码没报错。

渲染一个场景

清屏还算不上 3D。要渲染场景需要三样东西:装载渲染对象的 Scene、决定视角的相机, 以及把场景注册为活动渲染对象。

js
  // Create scene
  const scene = new Scene();
  // Create camera
  new PerspectiveCamera(scene, Math.PI / 3, 1, 100);
  // Set scene as active renderable object as layer 0
  getEngine().setRenderable(scene, 0);
  • new Scene() 创建场景容器。
  • new PerspectiveCamera(scene, fovY, near, far) 创建透视相机。宽高比是可选的第 5 个参数, 默认开启 autoAspect 会自动跟随渲染目标,通常不用传。需要正交投影时用 OrthoCamera
  • getEngine().setRenderable(scene, 0) 把场景设为第 0 层的活动渲染对象。 不做这一步,即使场景和相机都建好了画面上也不会有任何东西。

上例的相机没有赋值给变量却依然生效,因为场景还没有主相机时,新构造的相机会自动成为 该场景的 mainCamera。后面要操作相机时仍建议显式接住它。

让相机能操作

现在场景是空的、相机也不能动。加一个轨道控制器:

js
  scene.mainCamera = new PerspectiveCamera(scene, Math.PI / 3, 1, 100);
  // Set camera controller
  scene.mainCamera.controller = new OrbitCameraController({ center: new Vector3(0, 0, 1) });
  // Input handler middleware for camera controll
  getInput().use(scene.mainCamera.handleEvent, scene.mainCamera);

两行都必要:设 controller 只是装上控制器,还要用 getInput().use(...) 把它接入输入系统, 控制器才收得到事件。第二个参数是调用 handleEvent 时的 this

除此之外,控制器还需要每帧更新状态才能工作——它要靠这个时机做惯性、阻尼、平滑跟随之类的 逐帧计算。更新入口是 camera.updateController()

javascript
myApp.on('tick', function () {
  scene.mainCamera.updateController();
});

上面的示例里没有这行,是因为场景的 mainCamera 会被自动更新:场景每帧的更新流程中 会调用 mainCamera.updateController()。所以只用一个主相机时不必手动调。

但下面两种情况必须自己调,否则控制器装了也不起作用:

  • 相机不是场景的 mainCamera(例如多视口渲染里的副相机);
  • 你绕过 setRenderable(),自己在 tick 里调用 camera.render(scene) 驱动渲染, 且该相机不是 mainCamera

反过来,每个相机每帧只应更新一次。引擎不保证同一帧内重复调用是安全的,所以已经被自动 更新的主相机不要在 tick 里再手动调一次。

试着用鼠标左键拖动、滚轮缩放。

下一步

到这里你已经有一个可交互的空场景了。接着往里放东西:

Released under the MIT License.