在 1.0 之前,我们准备在 #4 基础上再实现如下改动,来大幅提升 Beam 的资源管理能力:
- 支持动态更新 Renderer 插件序列
- 支持跨插件可选复用数据 Buffer
- 支持跨插件可选复用 Index Buffer
- 支持简化的插件 Schema.from 更新机制
- 支持动态更新插件着色器
下面介绍特性概述、API 变更及开发进展
特性概述
动态更新 Renderer 插件序列
在目前,Beam 渲染器在 constructor 中分配固定的插件序列,它们无法在运行时动态改变,并且 pipelinePlugins
与 offscreenPlugins
引入了额外的概念,并造成了一些代码实现上不必要的重复。为了自由地在运行时切换着色器,我们做了 API 的改造。现有使用方式:
const pipelinePlugins = [meshPlugin, postEffectPlugin]
const offscreenPlugins = [filterPlugin]
const renderer = new Renderer(canvas, pipelinePlugins, offscreenPlugins)
我们引入了 Renderer.usePlugins
API,改造后:
const renderer = new Renderer(canvas)
renderer.usePlugins([meshPlugin, postEffectPlugin])
注意我们当前还没有重构离屏绘制的 API,故而 offscreenPlugins
能力相当于被暂时废弃了。我们将在未来重新设计实现这一特性。
跨插件可选复用数据 Buffer
在目前,我们将 Buffer 与 Shader 做了一对一的绑定。这主要适合插件渲染内容各自独立的场景。例如依次渲染这些内容:
但另一种常见的情形,是由多个着色器绘制同一份数据 Buffer。例如:
- 3D 模型有多种着色算法,需要在不同着色器中承载更新
- 后处理等场景下,需要多个 Pass 逐一渲染
目前 Beam 可以让元素 State 在插件之间复用,但每个 State 都会逐插件创建一份数据 Buffer。这导致了在展示 3D 模型线框辅助线的时候,数据量增加了一倍。在这个 PR 中,我们支持显式控制插件的数据 Buffer 共享,即 Beam.sharePluginResource
API。在 renderer 使用插件之前调用这一 API 即可,形如:
Beam.sharePluginResource([pluginA, pluginB]) // 声明 A 与 B 的共享关系
const renderer = new Renderer(canvas)
renderer.usePlugins([pluginA, pluginB, pluginC]) // 实际使用插件
- 若在初始化资源前调用该 API,则数据 Buffer 只会,其它使用方式完全不变。
- 被共享的插件,其 Buffer Schema 会被自动合并,若各插件字段无法正确合并,则优化无法生效。
- 不调用该 API 时,插件默认维护独立的 Buffer 资源。
跨插件可选复用 Index Buffer
上一复用数据 Buffer 的优化特性,并不能完全满足需求,例如:
- 在模型上叠加线框时,线框的连线式绘制,其需要的顶点顺序与原始的三角形方式不同
- 为模型的部分顶点添加描边时,描边数据并不会覆盖全部的模型,只是原有顶点的子集
- 多个着色插件可能完全共享一个大型的数据 Buffer,基于不同索引,各自绘制一部分的子集
对于这些需求,我们在 sharePluginResource
的基础上追加了可选的 Index Buffer 复用能力。默认情况下,被共享的插件使用完全相同的 Index。但只要为该 API 传入第二个 shareIndex = false
参数,即可在这些插件复用数据 Buffer 的基础上,使用各自独立的 Index Buffer 来绘制。以在模型上叠加线框为例:
// 初始化插件与 Renderer
const shapePlugin = new BasicShapePlugin()
const wireframePlugin = new WireframeShapePlugin()
const renderer = new Basic3DRenderer(canvas)
// 打开资源共享优化,注意第二个 false 参数
Beam.sharePluginResource([wireframePlugin, shapePlugin], false)
// 由 Renderer 初始化插件
renderer.usePlugins([wireframePlugin, shapePlugin])
// 每个 Shape 元素会被这两个插件共享
const createShapeElement = state => (
Beam.createElement(state, BasicShapePlugin, WireframeShapePlugin)
)
// 创建一个球体和三个周边的立方体
const shapeA = createShapeElement({ type: 'sphere', position: [0, 0, 0] })
const shapeB = createShapeElement({ position: [2, 0, 0] })
const shapeC = createShapeElement({ position: [-2, 0, 0] })
const shapeD = createShapeElement({ position: [0, -2, 0] })
// 加入元素并渲染
renderer.setCamera([0, 10, 10])
renderer.addElements([shapeA, shapeB, shapeC, shapeD])
renderer.render()
简化的插件 Schema.from 更新机制
目前,Beam 使用了 plugin.propsByElement
和 plugin.propsByEnv
两个生命周期钩子,来将语义化的 State 数据转换为 WebGL 所需的资源数据。这一实现虽然简单,但带来了一些优化和使用上的问题:
propsByElement
和 propsByEnv
所返回的 Key,在 Schema 中都已经存在,必须写两遍。
- 复用 Buffer 结构时,需要合并多个插件的
propsByElement
计算结果。这仍然需要进行多次潜在的高耗计算。
- 我们不知道
propsByElement
中的哪些字段是高耗的 Buffer 字段,哪些是普通的 Uniform 字段,难以做更细粒度的更新优化。
- 每个字段如果需要默认值,需要在钩子中自行控制,较为松散且不易二次定制。若插件使用者希望不修改插件源码的前提下更改字段默认值,则必须覆盖整个
propsBy
钩子。
- Props 概念的语义不如 Resources 概念符合直觉,但
resourcesByElement
看起来很啰嗦。
为此,我们借鉴了 REGL 的设计,引入新的 Schema 的 from
API 来替代现有的 propsBy
钩子。它不仅降低了使用成本,还利于提高性能。现有的 API 使用方式形如这样:
export class ShapePlugin extends ShadePlugin {
constructor () {
// ...
// 原有的 Schema 写法
this.resourceSchema = {
buffers: {
pos: { type: vec4, n: 3 },
color: { type: vec4 },
index: { type: index }
},
uniforms: {
modelMat: { type: mat4 },
viewMat: { type: mat4 },
projectionMat: { type: mat4 }
}
}
}
// 返回 Key 与 Schema 字段相互匹配的字段
propsByElement (state) {
const { type, position, color } = state
const modelMat = create()
if (state.translate) translate(modelMat, modelMat, state.translate)
if (type === 'sphere') {
const { positions, colors, indices } = getSphere(position)
return { modelMat, pos: positions, color: colors, index: indices }
} else {
const { positions, colors, indices } = getCube(position, color)
return { modelMat, pos: positions, color: colors, index: indices }
}
}
// 此处相当于简单的字段名转换
propsByEnv (state) {
return {
viewMat: state.camera,
projectionMat: state.perspective
}
}
}
而新的 Schema from
API 则将钩子整合到了 Schema 中,形如这样:
export class BasicShapePlugin extends ShadePlugin {
constructor () {
// ...
this.resourceSchema = {
buffers: {
// getPos / getColor / getIndices 用于转换 state 数据
pos: { type: vec4, n: 3, from: getPos },
color: { type: vec4, from: getColor },
index: { type: index, from: getIndices }
},
uniforms: {
// from 为字符串时,直接转换 state 的相应字段即可
viewMat: { type: mat4, from: 'camera' },
projectionMat: { type: mat4, from: 'perspective' }
}
}
}
}
这一改造使得框架能够追踪单个字段的更新,实现更细粒度的优化。同时,在简单情况下可以指定字符串形式的 from
转换方式,基于插件二次开发时也非常容易定制。
还有相关的两点更新:
- 每个 Schema 项可以配套一个
default
字段,作为默认值。
- 对 Textures 和 Uniforms 下的 Schema 项,它们还支持一个额外的
element: true
配置。这可以用于指定该字段是从 ElementState 还是 EnvState 中获取。
动态更新插件着色器
目前 Beam 的着色器是在 new Plugin
时固定,传入的 defines
宏也是固定的。这不利于较为灵活的着色器更新场景。为此,我们设计了 Plugin.setProgram
API 来增强灵活性。现有 API 形如:
const fooPlugin = new FooPlugin({ FOO: true }) // 使用固定的 defines
更改后的 API 则是这样的:
const fooPlugin = new FooPlugin()
fooPlugin.setProgram({ vs, fs, defines })
这里的 vs
和 fs
都是可选的着色器字符串。插件有自己默认的着色器,可以不提供。而 defines
则是可以动态更新的宏定义。在调用 API 后,框架即会使用新的着色器来进行渲染。
API 变更
这一 PR 主要对应如下 API 变更:
new Renderer(canvas, plugins)
改造为 new Renderer(canvas)
和 renderer.usePlugins
两步。
new ShadePlugin(defines)
改造为new ShadePlugin()
。
propsBy
风格 API 迁移到 from
风格。
开发进展
- 所有新增 API 均已可用。
- 自带示例中,Basic Shades / Textures / Lambert 已完成适配,PBR 等其它示例暂待跟进。
- FBO 处理暂未支持,需额外的 PR 设计实现。
- 文档暂未更新,预计需较大范围的重写。
- 特殊边界情况处理,待示例适配过程中完善。