基本框架
创建应用
首先我们必需创建一个应用对象。
注意,使用@zephyr3d/scene框架的项目必需有且只有一个应用实例!
当一个应用被创建以后,可以使用 getApp 全局函数来获取全局应用实例。
import { Application } from '@zephyr3d/scene';
import { backendWebGL2 } from '@zephyr3d/backend-webgl';
const myApp = new Application({
backend: backendWebGL2,
canvas: document.querySelector('#my-canvas')
});
myApp.ready().then(function () {
myApp.run();
});构造 Application 时的两个必要选项:
backend:渲染后端。目前可选backendWebGL、backendWebGL2(均来自@zephyr3d/backend-webgl) 和backendWebGPU(来自@zephyr3d/backend-webgpu)。canvas:用于渲染的画布元素。
ready() 返回一个 Promise,渲染设备初始化完成后 resolve;run() 启动主循环。上面这段代码 只是把渲染环境跑起来,帧循环里什么也没做,所以你会看到一个黑窗口。下面往循环里加内容。
添加帧事件响应
tick 事件在渲染循环的每帧触发一次,更新和渲染都写在它的处理函数里。
// 引入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();
});关键是第 20 行的 clearFrameBuffer(),它把整个画面清成绿色。三个参数依次是:
- 颜色缓冲区的清除颜色(
Vector4,RGBA); - 深度缓冲区的清除值——用
@zephyr3d/base导出的DEPTH_CLEAR_VALUE常量,不要硬编码 0 或 1。 引擎支持 reverse-Z 深度约定,近平面对应的深度值会随约定改变,这个常量保证两种约定下都正确(详见深度约定); - 模板缓冲区的清除值。
传 null 可以跳过对应缓冲区的清除。
现在你应该可以看到一个绿色的屏幕。
响应输入
用户输入通过和 tick 相同的方式监听。下面这个例子跟踪鼠标位置并把坐标画在屏幕上:
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 () {
let str = '';
// Define the clear color
const clearColor = new Vector4(0, 0, 0, 1);
// Set the font
myApp.device.setFont('16px arial');
// Frame event handler
myApp.on('tick', function () {
// Clears the frame buffer
myApp.device.clearFrameBuffer(clearColor, DEPTH_CLEAR_VALUE, 0);
// Render some text onto the screen
myApp.device.drawText(str, 30, 30, '#ffff00');
});
// handle pointer move
myApp.on('pointermove', function (ev) {
// Display text
str = `X:${ev.offsetX.toFixed()} Y:${ev.offsetY.toFixed()}`;
});
// The app has been initialized and the rendering loop begins
myApp.run();
});几个要点:
- 第 29 行的
pointermove处理函数只更新str变量,真正的绘制发生在第 26 行的tick里。 输入处理和渲染分开是有意的:事件可能一帧内触发多次,而绘制每帧只需要做一次。 - 第 18 行把
clearColor提到循环外复用。每帧new Vector4()会产生不必要的垃圾回收压力, 在数学对象上尤其值得注意。 drawText()需要先用setFont()设置字体(第 20 行)。
目前,画布会绑定以下事件并通过App透传给用户:
- pointerdown
- pointerup
- pointermove
- pointercancel
- keydown
- keyup
- keypress
- drag
- dragenter
- dragleave
- dragstart
- dragend
- dragover
- drop
- wheel
- compositionstart
- compositionupdate
- compositionend
很多情况下,我们处理输入事件的时候是有优先级的,例如在某些场合下,我们需要先处理UI部分的输入,在UI系统未处理该输入的情况下,我们才触发场景的点击。针对这种情况, 我们也提供了中间件模式。你可以依次注册事件处理函数作为中间件,当有用户输入的时候,中间件将会按照注册次序被依次调用,直到某个中间件函数返回true为止。如果所有中间件 都返回false,则通过Application.on注册的事件回调将被调用。下面是一个使用中间件的例子:
// 优先响应用户界面交互事件
getInput().use(function(evt, type) {
return processGUIEvent(evt, type);
});
// 如果用户界面未处理此事件(processGUIEvent方法返回false)则轮到此中间件,
getInput().use(function(evt, type) {
if(type === 'pointerdown') {
onPointerDown();
return true;
} else {
return false;
}
});渲染场景
渲染一个场景需要三样东西:装载渲染元素的 Scene、决定从哪个视角渲染的相机,以及把场景注册为 活动渲染对象。
import { Application, getEngine, PerspectiveCamera } from '@zephyr3d/scene';
import { backendWebGL2 } from '@zephyr3d/backend-webgl';
import { Scene } from '@zephyr3d/scene';
// 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 () {
// 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);
// The app has been initialized and the rendering loop begins
myApp.run();
});对照第 17、19、21 行:
new Scene()创建场景容器。new PerspectiveCamera(scene, ...)创建透视相机。参数依次是所属场景、垂直视场角(弧度)、 近裁剪面、远裁剪面;宽高比是可选的第 5 个参数,但相机默认开启autoAspect,会自动跟随 渲染目标的宽高比,通常不需要传。需要正交投影时改用OrthoCamera。getEngine().setRenderable(scene, 0)把场景设为第 0 层的活动渲染对象。这一步不做的话, 即使场景和相机都建好了,画面上也不会有任何东西。
注意这里的相机构造出来没有赋值给任何变量,场景却仍然渲染了:当场景还没有主相机时, 新构造的相机会自动成为该场景的 mainCamera。后面需要操作相机时(比如设置控制器) 仍然建议显式接住它,像下一节那样写成 scene.mainCamera = new PerspectiveCamera(...)。
以上代码渲染了一个空场景,效果如下:
摄像机控制
我们通过给摄像机设置控制器来实现对摄像机的控制。目前我们提供了两个控制器:
FPSCameraController
用于实现FPS射击游戏模式的摄像机控制,可以通过WSAD键和鼠标移动和转动摄像机。
OrbitCameraController
用于实现围绕目标点旋转和伸缩的摄像机控制。
下面我们为刚才的代码添加一个摄像机控制器:
import { Application, getEngine, getInput, OrbitCameraController, PerspectiveCamera } from '@zephyr3d/scene';
import { backendWebGL2 } from '@zephyr3d/backend-webgl';
import { Scene } from '@zephyr3d/scene';
import { Vector3 } from '@zephyr3d/base';
// 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 () {
// Create scene
const scene = new Scene();
// Create camera
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);
// Set scene as active renderable object at layer 0
getEngine().setRenderable(scene, 0);
// Start application
myApp.run();
});相比上一节只多了两行(第 22 和 24 行):
scene.mainCamera.controller = new OrbitCameraController({ center: ... })给相机装上控制器。center指定环绕的中心点。getInput().use(scene.mainCamera.handleEvent, scene.mainCamera)把控制器接入输入系统。 只设controller而不注册这一行,控制器收不到输入,相机不会响应鼠标。 第二个参数是 调用handleEvent时的this。
以下是运行效果, 尝试用鼠标左键控制摄像机的观察角度: