跳到主要内容

自定义效果

CameraFrame 通过继承 CameraFrameEffect 的效果对象执行后处理。cameraFrame.bloom、cameraFrame.vignette 等内置项也是效果类实例。可以自行编写效果,与内置项一起注册。

多数效果向合成贡献着色器代码块:所有代码块依次在绘制最终图像的单个通道中运行,不额外增加全屏通道。低分辨率模糊等无法每像素单次采样完成的效果还需拥有通道,参见带通道的效果。

合成插槽​

合成着色器对每个输出像素执行一次,读取场景颜色并转换为屏幕输出。场景渲染任意亮度、与光量成正比的线性 HDR 颜色,经色调映射进入显示范围,再进行伽马编码。效果代码块在过程中修改颜色。

合成插槽是转换过程中的某个调用点。每个效果在四个插槽之一应用,这决定接收到的颜色及可完成的工作。同一插槽效果依次执行,每个接收上一个返回的颜色:

图中按顺序显示各插槽及其内置效果。

插槽接收的颜色用途
COMPOSESLOT_SCENE刚采样的场景颜色及 alpha,尚未被后续插槽修改。修改场景图像本身,为其他效果提供基础,例如重采样附近像素的锐化,或修改 alpha。若在后续插槽重采样,会丢弃之前效果的修改。
COMPOSESLOT_HDR色调映射前的线性 HDR 颜色。处理光量,例如加入泛光或用 AO 压暗。
COMPOSESLOT_LDR色调映射后、伽马校正前,显示器 0–1 范围颜色。最终外观效果,例如暗角和颜色查找表。
COMPOSESLOT_OUTPUT伽马校正后,写入屏幕的最终值。对精确输出值操作,例如抖动。

SCENE 和 HDR 均为线性 HDR,之间无转换。所有 SCENE 效果先于 HDR 效果,且只有 SCENE 能修改输出 alpha。

SCENE 插槽代码块入口接收带 alpha 的 vec4 颜色和 UV,返回 vec4;其他插槽接收 vec3 颜色和 UV,返回 vec3。

内置效果​

CameraFrame 按下列顺序注册内置效果,这也是同插槽的应用顺序。每个效果由 id 标识,供 getEffect 和 insertEffectBefore 使用:

Id效果合成插槽
taa时间抗锯齿,cameraFrame.taa无,只拥有通道
fringing色边,cameraFrame.fringingCOMPOSESLOT_SCENE
cas锐化,cameraFrame.rendering.sharpnessCOMPOSESLOT_SCENE
dof景深,cameraFrame.dofCOMPOSESLOT_HDR
ssao环境光遮蔽,cameraFrame.ssao使用 SSAOTYPE_COMBINE 时为 COMPOSESLOT_HDR
volumetricFog体积雾,cameraFrame.volumetricFog无,只拥有通道
bloom泛光,cameraFrame.bloomCOMPOSESLOT_HDR
colorEnhance颜色增强,cameraFrame.colorEnhanceCOMPOSESLOT_HDR
grading颜色分级,cameraFrame.gradingCOMPOSESLOT_HDR
colorLut颜色查找表,cameraFrame.colorLUTCOMPOSESLOT_LDR
vignette暗角,cameraFrame.vignetteCOMPOSESLOT_LDR

新效果按添加顺序,在对应插槽的内置效果之后执行。其他引擎效果也如此:镜头光晕 lensFlare 在 HDR 插槽颜色分级之后执行,动态天空、SMAA 和自动曝光只拥有通道。要先于内置效果执行,用 insertEffectBefore 注册,参见注册效果。例如插到 bloom 之前,会先修改场景,再加入泛光,之后颜色分级作用于结果。

编写效果​

构造效果时传入图形设备、id 和声明,包括合成插槽及 GLSL、WGSL 代码块。代码块声明 uniform 和 apply<Id> 入口,由合成在对应插槽调用。下面将图像绘制为圆点:

uniform float pixelationTilePixels;
uniform float pixelationIntensity;

vec3 applyPixelation(vec3 color, vec2 uv) {
vec2 tileUV = vec2(pixelationTilePixels) * sceneTextureSize.zw;
vec2 centerUv = (floor(uv / tileUV) + 0.5) * tileUV;

vec2 local = (uv - centerUv) / tileUV;
float dist = length(local);
float edge = fwidth(dist) * 1.5;
float mask = 1.0 - smoothstep(0.35, 0.35 + edge, dist);
return mix(color, color * mask, pixelationIntensity);
}

效果类将参数声明为字段,并在 update 中应用:

class PixelationEffect extends CameraFrameEffect {
// the size of one tile, in screen pixels
tilePixels = 8;

// the blend between the original image (0) and the pixelated one (1)
intensity = 0.5;

constructor(device) {
super(device, 'pixelation', {
slot: COMPOSESLOT_LDR,
glsl: pixelationGLSL,
wgsl: pixelationWGSL
});
}

// at zero intensity, the effect leaves the image as it is, so it is left out of the shader
get active() {
return this.enabled && this.intensity > 0;
}

update() {
this.setUniform('pixelationTilePixels', this.tilePixels);
this.setUniform('pixelationIntensity', this.intensity);
}
}

数据驱动效果无需新类,直接用声明构造 CameraFrameEffect,再设置 uniform 即可。

代码块可以读取什么​

每个代码块可读取合成提供的场景颜色、sceneTexture,以及包含纹理宽、高及其倒数的 sceneTextureSize。纹素偏移、像素大小和宽高比等分辨率相关计算应使用该尺寸。

在 requires 中列出 FRAMERESOURCE_DEPTH 的效果,还可通过 getLinearScreenDepth(uv) 读取场景深度。

注册效果​

通过 addEffect 注册,同插槽按注册顺序执行。例如同属 LDR 插槽、在内置暗角后添加的效果,会在暗角之后执行。要放到另一效果之前,用 insertEffectBefore,传入目标效果或其 id,参见内置效果:

const pixelation = new PixelationEffect(app.graphicsDevice);
cameraFrame.addEffect(pixelation);

// or, to apply it before the vignette
cameraFrame.insertEffectBefore(pixelation, 'vignette');

cameraFrame.update();

getEffect 按 id 返回已注册效果,removeEffect 移除。

与 CameraFrame 其他配置一样,参数变化及增删效果均在下一次 CameraFrame#update 生效。CameraFrame 不拥有自定义效果,移除效果或销毁 CameraFrame 不会销毁它,因此不再使用时需调用 destroy。

参数​

效果在 update 应用参数,活动期间每次 CameraFrame#update 都会调用:

  • setUniform 设置代码块读取的 uniform。像材质参数一样按引用保存,因此数组或纹理可原地更新。
  • setDefine 为合成着色器设置变体宏。更改宏会重建着色器,因此只用于很少变化的设置,其余用 uniform。固定宏可在构造函数设置一次。

enabled 控制开关。强度为零等情况下无作用的效果,可覆盖 active,让代码块不进入合成着色器。

少数依赖时间等逐帧值的效果需实现 frameUpdate。每个参与帧都会收到本帧数据,包括距上一帧时间 dt 和场景渲染尺寸:

frameUpdate(frame) {
this.time += frame.dt;
this.setUniform('waveTime', this.time);
}

像自动曝光一样调整相机曝光的效果,通过 cameraExposureScale 返回缩放系数。

调试视图​

效果可用中间结果替代相机输出。将名称列入 debugViews,在代码块实现相应 debug<Name>() 函数,返回显示颜色:

super(device, 'pixelation', {
slot: COMPOSESLOT_LDR,
glsl, wgsl,
debugViews: ['pixelationMask']
});

将名称设置为 CameraFrame 调试视图以显示:

cameraFrame.debug = 'pixelationMask';
cameraFrame.update();

覆盖效果代码块​

效果代码块与内置块一样可按名称覆盖:在 ShaderChunks 中设置 compose<Id>PS 即可替换源码,也适用于内置效果:

ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_GLSL).set('composePixelationPS', myPixelationGLSL);
ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_WGSL).set('composePixelationPS', myPixelationWGSL);

效果活动时,合成着色器会定义其 id 对应的大写下划线宏,此例为 PIXELATION,供其他代码块检测。

编辑器中的效果​

编写继承 camera-frame-effect.mjs 的 CameraFrameEffectScript 脚本,与 cameraFrame 放在同一相机实体。将参数声明为属性,在 createEffect 创建效果,在 updateEffect 复制属性:

import { CameraFrameEffectScript } from './camera-frame-effect.mjs';

class PixelationEffectScript extends CameraFrameEffectScript {
static scriptName = 'pixelationEffect';

/**
* @attribute
* @range [0, 1]
*/
intensity = 0.5;

createEffect(device) {
return new PixelationEffect(device);
}

updateEffect(effect) {
effect.intensity = this.intensity;
}
}

两个脚本均存在时,效果立即注册,跟随脚本启用状态,并随脚本销毁。要暴露调试视图,覆盖脚本的 debugView getter。引擎仓库中的镜头光晕、SMAA 和自动曝光脚本都是完整示例。

示例​

LDR 插槽的像素化效果,在内置暗角之后应用。

自定义合成着色器