Skip to content

fix(rendering): size gaussian blur and bloom in css pixels - #694

Merged
stormmuller merged 2 commits into
devfrom
claude/zen-bardeen-v71su7
Oct 3, 2026
Merged

stormmuller merged 2 commits into
devfrom
claude/zen-bardeen-v71su7

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

Since 0.25.6 the canvas renders at the display's devicePixelRatio, so render targets are sized in device pixels. The Gaussian blur and bloom systems both sized their kernels in texels of those targets. The same settings therefore looked different on every display: at 200%, blur and glow spread less far on screen, and bloom was much brighter close to small bright sprites. Both effects are now sized in CSS pixels. Output at a pixel ratio of 1 is unchanged.

Gaussian blur. Each kernel tap is now one CSS pixel apart. The issue suggested multiplying u_texelSize by pixelRatio, but that spaces taps 2 texels apart at a ratio of 2. Odd and even device columns would then never mix, which is the banding gaussian-blur.md already warns about. Instead, when pixelRatio > 1:

  • the scene is box-downsampled to CSS-pixel resolution with a new box-downsample.frag;
  • the blur passes run at that resolution, where one CSS pixel is one texel;
  • the last vertical pass upsamples straight back into the camera's renderTarget.

This also makes HiDPI blur cheaper, since its cost now stays close to what it is at a ratio of 1.

The blend path was restructured as a side effect. A fractional intensity now keeps the sharp scene in renderTarget, and the last pass leaves the blur in the ping-pong pair. That removes the separate snapshot copy, so blending costs 2 extra draws instead of 3.

Bloom.

  • The downsample block is now round(4 * pixelRatio) texels, which is 4×4 CSS pixels.
  • bloom-threshold.frag takes a u_blockSize uniform and thresholds every texel in the block before averaging. Before, it took a fixed 4×4 set of samples.
  • The blur step is worked out from the full-resolution target, so rounding the block size doesn't change how far the glow reaches.

Not done: the issue's optional idea of also scaling with window size, by taking the kernel step as a fraction of the target's height. CSS-pixel sizing matches how the rest of the engine sizes things, such as text outlines and UI constantPixelSize.

Evidence

A new e2e spec (e2e/specs/post-process-pixel-ratio.spec.ts) renders a small white square with each effect at device scale factors 1 and 2. It compares the on-screen luminance profiles in CSS pixels, as a relative, same-run measurement.

  • Old code: the spec fails. With bloom, luminance just outside the square is 75 at a ratio of 2 vs 33 at 1, and the glow ends at 15 CSS px vs 24. With the blur, it ends at 8 CSS px vs 12.
  • New code: both profiles match within 1–2 luminance levels.

While writing this, the e2e also caught a read/write feedback loop in my first version of the blur loop. A new unit test now checks that no draw samples the texture it is drawing into.

Related issue(s)

Defect 008: "Blur and bloom are sized in device pixels, so their look changes with the display's pixel ratio".

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (1747 tests)
  • npm run lint passes with 0 errors (2 existing TODO warnings)
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • Any new/changed public API is exported from the module's index.ts: boxDownsampleFragmentShader is exported from the shaders module and registered in createShaderCache
  • Documentation under /documentation-site/docs/docs is updated: bloom.md and gaussian-blur.md (new "Same look on every display" section, updated performance notes)
  • Demo verified:
    • ran npm run build at the repo root;
    • from documentation-site/, npm run build and npm run typecheck pass;
    • the space-shooter demo, which uses both effects, renders correctly at device scale 1 and 2 with no engine errors.
  • The full e2e suite passes locally (38 tests)

Changelog

  • A bullet has been added under ## [Unreleased] in CHANGELOG.md (#### Fixed)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BiAVgvaR1vQhqx1xN871Np


Generated by Claude Code

claude and others added 2 commits October 3, 2026 10:06
Since rendering moved to the display's device pixel ratio, render targets
are sized in device pixels, and both effects sized their kernels in texels
of those targets, so the same settings looked different on every display.

- Gaussian blur steps one CSS pixel per tap. On a high-DPI display it
  box-downsamples the scene to CSS-pixel resolution, blurs there, and
  upsamples on the last pass, rather than spacing taps pixelRatio texels
  apart (which skips texels and stripes thin details).
- Bloom's downsample block is 4 CSS pixels (4 * pixelRatio texels,
  rounded). The threshold pass averages every texel in the block, and the
  blur step is computed from the full-resolution target so rounding the
  block doesn't change the glow's reach.
- Adds an e2e spec comparing each effect's on-screen luminance profile at
  device scale factors 1 and 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiAVgvaR1vQhqx1xN871Np
@stormmuller
stormmuller enabled auto-merge (squash) October 3, 2026 10:09
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@stormmuller
stormmuller merged commit 8df6811 into dev Oct 3, 2026
13 checks passed
@stormmuller
stormmuller deleted the claude/zen-bardeen-v71su7 branch October 3, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants