跳到主要内容

带通道的效果

自定义效果可拥有帧通道,完成合成阶段每像素单次采样无法做到的工作,例如低分辨率模糊、向场景绘制或跨帧累积。内置泛光、景深和时间抗锯齿都属于带通道效果。

CameraFrame 向效果请求通道、提供所需帧资源(如场景深度),并在对应阶段执行。效果像普通渲染通道一样编写,通常使用着色器四边形通道,效果代码块可以读取它们的输出。拥有通道的效果不一定需要合成插槽。

CameraFrame 的阶段​

CameraFrame 在场景渲染通道之间的五个阶段执行效果通道:

双边框方框标识效果通道执行阶段。

阶段执行时机用途引擎效果
preScene有深度预通道时在其之后,场景之前。渲染供场景材质读取的纹理。SSAOTYPE_LIGHTING 类型 AO、动态天空
postOpaque不透明几何体、天空盒及场景颜色复制之后,透明几何体之前。向场景绘制,位于透明几何体下方。无
postScene场景之后、解析之前。向场景绘制。SSAOTYPE_COMBINE 类型 AO、体积雾
resolve解析场景时。读取场景颜色并输出新颜色,例如抗锯齿,参见解析场景。TAA、SMAA
postResolve场景解析并生成半分辨率副本之后,合成之前。处理最终场景颜色,例如泛光。景深、泛光、镜头光晕、自动曝光

各阶段按效果注册顺序执行:先按内置效果所列顺序,再按添加顺序执行自定义效果,除非通过 insertEffectBefore 插到其他效果之前。

帧资源​

效果在 requires 中声明资源需求,活动期间由 CameraFrame 提供:

资源提供内容
FRAMERESOURCE_DEPTHresources.depth,场景深度。
FRAMERESOURCE_PREPASSDEPTHresources.prepassDepth,场景前由深度预通道生成的深度,供 preScene 通道使用。
FRAMERESOURCE_SCENECOLORHALFresources.sceneColorHalf,解析后场景颜色的半分辨率副本。
FRAMERESOURCE_SCENECOLORGRABresources.sceneColorGrab,不透明场景颜色副本的句柄,从 postOpaque 开始可作为 uSceneColorMap 采样。
FRAMERESOURCE_SCENETARGETresources.sceneTarget,场景渲染到的目标,供向场景绘制的通道使用。

资源对象在通道生命周期内不变,可一次性交给通道。但纹理会随画布调整大小,因此不要缓存尺寸。需求依赖参数时,可将 requires 覆盖为 getter。

场景深度的存储方式取决于生成方式,因此着色器应通过 screenDepthPS 的 getLinearScreenDepth 读取,并提供描述存储方式的宏。将深度宏传给着色器,并将其键加入唯一着色器名,避免不同深度格式共享编译结果:

const { depth } = resources;
this.shader = ShaderUtils.createShader(device, {
uniqueName: `InkOutlineShader-${depth.key}`,
attributes: { aPosition: SEMANTIC_POSITION },
vertexChunk: 'quadVS',
fragmentDefines: depth.defines,
fragmentGLSL: inkGLSL, // includes "screenDepthPS", and calls getLinearScreenDepth(uv)
fragmentWGSL: inkWGSL
});

创建通道​

效果在 createPasses 中根据所需资源创建通道,按执行顺序添加到相应阶段数组。一个效果可跨多个阶段。在 destroyPasses 中释放:

class InkOutlineEffect extends CameraFrameEffect {
width = 3;

_pass = null;

constructor(device) {
super(device, 'inkOutline', {
requires: [FRAMERESOURCE_SCENECOLORGRAB, FRAMERESOURCE_SCENETARGET, FRAMERESOURCE_DEPTH]
});
}

createPasses(resources, passes) {
// the pass draws the lines into the scene, after the opaque geometry
this._pass = new RenderPassInkOutline(this.device, this.cameraFrame.cameraComponent, resources.depth);
this._pass.init(resources.sceneTarget);
passes.postOpaque.push(this._pass);
}

destroyPasses() {
this._pass?.destroy();
this._pass = null;
}

// called by cameraFrame.update(): apply the parameters to the pass
update() {
this._pass.width = this.width;
}
}

向自有目标渲染的通道应通过 resizeSource 设置尺寸,例如跟随半分辨率场景。纹理按引用保存,因此效果代码块可通过 setUniform 设置的 uniform 读取通道输出。

何时重建通道​

CameraFrame 构建自身通道时也创建效果通道:首次 CameraFrame#update,及之后每次重建时,先通过 destroyPasses 释放旧通道再创建。触发条件包括配置变化,带通道效果的增删、启用或禁用,以及其资源需求或 buildKey 变化。

buildKey 返回字符串,在需要重建时变化;应包含改变通道结构的参数,而非 uniform。通过 update 获取的参数不应放入其中:

buildKey() {
// a pass more renders at the high quality
return `${this.highQuality}`;
}

效果没有通道时也会调用 destroyPasses,例如构建时未活动的效果,或销毁效果时。因此只销毁实际存在的对象,并像示例一样清空引用。

逐帧值​

资源对象不会逐帧变化,但场景分辨率、TAA 在两张纹理之间交替的解析颜色等值会变化。它们会在任何通道渲染前,每帧传给 frameUpdate:

frameUpdate(frame) {
// the size of a pixel of the scene render target, which the pass renders into
this._pass.texelWidth = 1 / frame.renderWidth;
this._pass.texelHeight = 1 / frame.renderHeight;
}

解析场景​

在 resolve 阶段执行的效果会以自身输出替换后续帧步骤读取的场景颜色。CameraFrame 每帧在 frameUpdate 之前调用 resolveScene,传入待解析场景。效果将之前效果解析的颜色交给通道,再替换为本帧通道输出纹理:

resolveScene(scene) {
this._pass.setSourceTexture(scene.texture);
scene.texture = this._pass.outputTexture;
}

若解析到不同分辨率,例如上采样器,还需设置场景的 width 和 height。

TypeScript​

TypeScript 不会根据基类自动推导覆盖方法的参数类型。需导入钩子类型 CameraFrameEffectResources、CameraFrameEffectPasses、CameraFrameEffectScene 和 CameraFrameEffectContext,进行注解:

import { CameraFrameEffect } from 'playcanvas';
import type { CameraFrameEffectPasses, CameraFrameEffectResources } from 'playcanvas';

class BlurEffect extends CameraFrameEffect {
createPasses(resources: CameraFrameEffectResources, passes: CameraFrameEffectPasses) {
// ...
}
}

示例​

墨线轮廓效果在不透明场景深度或亮度突变处画线。通道位于 postOpaque,读取不透明场景颜色副本与深度,并向场景绘制,因此透明几何体会显示在线条上方。

墨线轮廓