Render passes and offscreen targets
Quick answer
This tutorial explains that a render pass decides where a batch of draws lands, either the canvas or a framebuffer. It builds a 256 by 256 color texture and depth texture, wraps them in a Framebuffer, renders the scene offscreen, then samples the color attachment through a compositor Model into the screen pass. It stresses attachment usage flags and readBuffer for pixel readback. Readers can add post-processing, picking or multi-pass composition.
Key points
- Give color attachments both Texture.SAMPLE and Texture.RENDER usage.
- End each pass before starting the next, since nesting corrupts viewport state.
- Read pixels through colorTex.readBuffer followed by readAsync on a Buffer.
Example
const SIZE = 256;
const colorTex = device.createTexture({
width: SIZE,
height: SIZE,
format: 'rgba8unorm',
usage: luma.Texture.SAMPLE | luma.Texture.RENDER | luma.Texture.COPY_DST,
sampler: { minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'none' }
});
const depthTex = device.createTexture({
width: SIZE,
height: SIZE,
format: device.preferredDepthFormat,
usage: luma.Texture.RENDER
});
const fbo = device.createFramebuffer({
width: SIZE,
height: SIZE,
colorAttachments: [colorTex],
depthStencilAttachment: depthTex
});
const compositor = new luma.Model(device, {
vs: quadVS,
fs: compositeFS,
attributes: { positions: quadPositions, uvs: quadUVs },
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x2' },
{ name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
],
bindings: { uScene: colorTex },
vertexCount: 6,
topology: 'triangle-list'
});
function render() {
const offscreen = device.beginRenderPass({
framebuffer: fbo,
clearColor: [0.02, 0.04, 0.07, 1],
clearDepth: 1
});
sceneModel.draw(offscreen);
offscreen.end();
const screen = device.beginRenderPass({
clearColor: [0.04, 0.06, 0.09, 1]
});
compositor.draw(screen);
screen.end();
}const pickTex = device.createTexture({
width: width,
height: height,
format: 'rgba8unorm',
usage: luma.Texture.SAMPLE | luma.Texture.RENDER |
luma.Texture.COPY_SRC | luma.Texture.COPY_DST,
sampler: { minFilter: 'nearest', magFilter: 'nearest' }
});
const pickFbo = device.createFramebuffer({
width: width,
height: height,
colorAttachments: [pickTex],
depthStencilAttachment: device.createTexture({
width: width,
height: height,
format: device.preferredDepthFormat,
usage: luma.Texture.RENDER
})
});
Notes
- Sampling the same texture you are rendering to creates a forbidden feedback loop.
- A missing pass.end() leaks the old viewport and clear settings into later draws.
English edition written for Atlaslog. Code is taken from the original Chinese page and runs unchanged.
Atlaslog (2026). Render passes and offscreen targets. https://www.jingweizhi.com/en/lumagl/tutorials/render-passes.html (accessed 2026-11-03).