Const Advancedclass MyEdgePass extends Pass3D {
public readonly stage = 'afterTransparent';
public readonly requiredOutputs = [normalOutput];
public build(graph: RenderGraph, ctx: PassContext): void {
const normals = graph.getSceneOutput(normalOutput);
graph.addPass(new MyEdgeDetectPass({ renderer: ctx.renderer, normals }));
}
}
Each pixel's surface normal in the camera's view space, in an
rgba8unormtextureThe normal is the one lighting used, after normal maps and flat shading, packed from -1 to 1 into 0 to 1: read it back with
rgb * 2 - 1. View space puts+xright,+yup and+ztowards the camera, on both backends and whatever the target. Alpha is 1 where a surface wrote the pixel and 0 where nothing did, such as the background. On a multisampled scene an edge pixel holds the average of its samples, so alpha is the share of the pixel that surfaces cover: dividergbby alpha before unpacking, then normalize.Opaque and glass surfaces write it, as they write depth, so the two agree; blended and refractive surfaces leave the normal behind them, and so does a material with a template of its own. For effects that want the surface's shape, such as ambient occlusion or edge detection, it beats rebuilding normals from depth: it's smooth across curves and keeps normal-map detail.
It costs every opaque and glass pixel one more 4-byte write while a pass uses it. On a canvas with
antialiasit's multisampled with the scene on WebGPU; on WebGL a view that writes it draws its scene single-sample.