Describe the bug π
With PostproductionRenderer, the scene is drawn into the EffectComposer's ping-pong WebGLRenderTargets. The composer is created without a render target (packages/front/src/core/PostproductionRenderer/src/index.ts#L382):
this.composer = new EffectComposer(this._renderer.three);
so three.js allocates them with samples = 0. The hardware MSAA of the default framebuffer (antialias: true) therefore only applies while postproduction.enabled === false; the moment postproduction is switched on, every geometry edge is rendered without multisampling. SMAA (smaaEnabled) recovers only part of it. The visible result is that the postproduction image looks "noisier" / more jagged than the plain render, and the usual workaround people reach for β pixel ratio 2 β does not even supersample the postproduction image: setPostproductionSize() applies the DPR only in its argument-less init path, and every onResize puts the composer back to CSS size, so the composer output is merely upscaled to the backbuffer.
What I expect: enabling postproduction does not make edges worse than the plain renderer.
What actually happens: postproduction.composer.renderTarget1.samples === 0; edges are aliased.
Measurement (AR model of an office building, fixed exterior view, Chrome / AMD Radeon 8060S, 1665Γ1123). "Hard edges" = share of neighbouring pixel pairs in a faΓ§ade region whose luma differs by more than 60 β a repeatable aliasing proxy; the pixel-ratio-2 frame was box-filtered 2Γ2 to screen resolution before comparing.
|
hard edges |
render ms |
postproduction on, composer samples = 0 (current) |
43.5 % |
12.2 |
postproduction on, composer samples = 4 |
33.7 % |
12.8 |
| postproduction off (default framebuffer, MSAA) |
36.2 % |
9.9 |
postproduction on, samples = 0, pixel ratio 2 (composer stays at CSS size, output upscaled) |
33.6 % |
13.2 |
Multisampled composer targets bring the hard-edge share to the level of the pixel-ratio-2 output β which, per the above, is an upscale rather than supersampling β for ~+0.6 ms per frame, at the composer's own resolution.
Proposed fix β pass a multisampled target to the composer (three β₯ r163 is WebGL2-only, so samples is always honoured; renderTarget2 is cloned from it and inherits samples):
const size = this._renderer.three.getSize(new THREE.Vector2());
const pixelRatio = this._renderer.three.getPixelRatio();
const target = new THREE.WebGLRenderTarget(size.x * pixelRatio, size.y * pixelRatio, {
type: THREE.HalfFloatType, // EffectComposer's default
samples: Math.min(4, this._renderer.three.capabilities.maxSamples),
});
this.composer = new EffectComposer(this._renderer.three, target);
The composer targets carry no depthTexture (the AO / edge-detection passes keep their own single-sample targets), so this is safe; Postproduction.setSize() already forwards to composer.setSize(), which preserves samples. Optionally expose it as postproduction.samples (default 4, 0 = current behaviour).
Workaround on the app side until then, applied once after postproduction.enabled = true:
for (const t of [pp.composer.renderTarget1, pp.composer.renderTarget2]) {
if (t && t.samples !== 4) { t.samples = 4; t.dispose(); } // re-allocated multisampled on next use
}
Reproduction βΆοΈ
Any PostproductionRenderer tutorial from docs.thatopen.com β the check is postproduction.composer.renderTarget1.samples in the console (returns 0).
Steps to reproduce π’
- Load a model in a world with
PostproductionRenderer; leave the default COLOR style.
- Compare a geometry edge with
postproduction.enabled = false vs true (SMAA on or off) β edges get visibly more jagged when enabled.
postproduction.composer.renderTarget1.samples β 0.
- Apply the workaround above β edges match the non-postproduction render.
System Info π»
Browsers: Chrome (Windows, AMD Radeon 8060S); also reproduced in headless Chromium on Linux (Mesa)
npmPackages:
@thatopen/components: 3.4.8
@thatopen/components-front: 3.4.4
@thatopen/fragments: 3.4.7
three: 0.184.0
Used Package Manager π¦
npm
Validations β
Edit 2026-09-07: corrected the pixel-ratio-2 remarks. With postproduction on, the composer stays at CSS size after any resize (the DPR multiplication in setPostproductionSize() only runs in the argument-less init path), so the pixel-ratio-2 row above was an upscaled CSS-size render, not supersampling, and the earlier "4Γ fill" remark was wrong. Pointed out by an independent reviewer on the forum; the MSAA measurements are unaffected (same resolution in every row).
Describe the bug π
With
PostproductionRenderer, the scene is drawn into theEffectComposer's ping-pongWebGLRenderTargets. The composer is created without a render target (packages/front/src/core/PostproductionRenderer/src/index.ts#L382):so three.js allocates them with
samples = 0. The hardware MSAA of the default framebuffer (antialias: true) therefore only applies whilepostproduction.enabled === false; the moment postproduction is switched on, every geometry edge is rendered without multisampling. SMAA (smaaEnabled) recovers only part of it. The visible result is that the postproduction image looks "noisier" / more jagged than the plain render, and the usual workaround people reach for β pixel ratio 2 β does not even supersample the postproduction image:setPostproductionSize()applies the DPR only in its argument-less init path, and everyonResizeputs the composer back to CSS size, so the composer output is merely upscaled to the backbuffer.What I expect: enabling postproduction does not make edges worse than the plain renderer.
What actually happens:
postproduction.composer.renderTarget1.samples === 0; edges are aliased.Measurement (AR model of an office building, fixed exterior view, Chrome / AMD Radeon 8060S, 1665Γ1123). "Hard edges" = share of neighbouring pixel pairs in a faΓ§ade region whose luma differs by more than 60 β a repeatable aliasing proxy; the pixel-ratio-2 frame was box-filtered 2Γ2 to screen resolution before comparing.
samples = 0(current)samples = 4samples = 0, pixel ratio 2 (composer stays at CSS size, output upscaled)Multisampled composer targets bring the hard-edge share to the level of the pixel-ratio-2 output β which, per the above, is an upscale rather than supersampling β for ~+0.6 ms per frame, at the composer's own resolution.
Proposed fix β pass a multisampled target to the composer (three β₯ r163 is WebGL2-only, so
samplesis always honoured;renderTarget2is cloned from it and inheritssamples):The composer targets carry no
depthTexture(the AO / edge-detection passes keep their own single-sample targets), so this is safe;Postproduction.setSize()already forwards tocomposer.setSize(), which preservessamples. Optionally expose it aspostproduction.samples(default 4, 0 = current behaviour).Workaround on the app side until then, applied once after
postproduction.enabled = true:ReproductionβΆοΈ
Any PostproductionRenderer tutorial from docs.thatopen.com β the check is
postproduction.composer.renderTarget1.samplesin the console (returns 0).Steps to reproduce π’
PostproductionRenderer; leave the default COLOR style.postproduction.enabled = falsevstrue(SMAA on or off) β edges get visibly more jagged when enabled.postproduction.composer.renderTarget1.samplesβ0.System Info π»
Used Package Manager π¦
npm
Validations β
samples; the composer is just created without them).Edit 2026-09-07: corrected the pixel-ratio-2 remarks. With postproduction on, the composer stays at CSS size after any resize (the DPR multiplication in
setPostproductionSize()only runs in the argument-less init path), so the pixel-ratio-2 row above was an upscaled CSS-size render, not supersampling, and the earlier "4Γ fill" remark was wrong. Pointed out by an independent reviewer on the forum; the MSAA measurements are unaffected (same resolution in every row).