Repository navigation
Effects: a chain measures, places and clips as one effect does - #1793
Merged
Merged
Conversation
A renderable drawn through two or more effects (or one, on a UIPanel, ProgressBar or Trail, which set postEffectNeedsCapture) is drawn into a capture the size of the canvas, and its effects run over that. Four things went wrong there: - Widths, offsets and sizes (glow, outline, blur, drop shadow, chromatic aberration, pixelate, bloom) were measured in the capture's pixels, the screen's, where one effect measures in the sprite's own: at 0.13 scale, adding a second effect made an outline about 8 times thicker. Each pass is now handed the size of the renderable's pixels on the target (Renderer#_effectScaleOf), and measures in those. - Under a camera's zoom (or one drawn away from the screen's corner), the passes placed patterns (vignette, gradient, scanline curve, noise_uv) by the renderable's transform alone, though a pass runs in the target's pixels, which the projection puts it on: they were misplaced or missing. The chain's space now maps through the projection too (Renderer#_drawnTransform), as does the pixel size. - The capture drops the clip it is drawn under, and the composite drew before the clip was back, so a minimap's sprites with effects spilled over the main view, and out of a clipping container. The composite now draws inside the clip the pass began under (_resumeSavedClip). - WebGPU read a capture through a fixed linear sampler, WebGL through whatever sampler the last draw left on the unit. Both now read it at the game's texture filter, as a sprite reads its own texture, so a pixel-art game stays crisp on WebGPU too. Camera effects keep measuring in screen pixels, and an effect given a textureSize keeps it. The sprite example's two chained looks are re-tuned for the new units and look as they did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A renderable drawn through two or more effects (or one, on a
UIPanel,ProgressBarorTrail, which setpostEffectNeedsCapture) is drawn into a capture the size of the canvas, and its effects run over that capture. Four things went wrong on that path. This fixes all of them, so a chain measures, places and clips the way a single effect does.1. Widths came out in screen pixels. Glow, outline, blur, drop shadow, chromatic aberration, pixelate and bloom measured their width, offset or size in the capture's pixels. A single effect measures in the sprite's own pixels, so at 0.13 scale, adding a second effect made an outline about 8 times thicker.
Renderer#_effectScaleOf).ShaderEffect#_clearFrameRectdivides the target size by it.2. Patterns were misplaced under a camera's zoom. In a chain, the passes placed patterns (vignette, gradient, scanline curve,
noise_uv) using the renderable's transform alone. A pass actually runs in the target's pixels, and the projection decides where the renderable lands there, so under zoom or a camera drawn away from the screen's corner the patterns were misplaced or missing. The chain's space now maps through the projection too (Renderer#_drawnTransform), and so does the pixel size. This bug came from #1789, which hasn't been released.3. The composite ignored the clip. The capture drops the clip the renderable is drawn under, and the composite drew before that clip was restored. So a minimap's sprites with effects spilled over the main view, and out of a clipping container. The composite now draws inside the clip the pass began under (
_resumeSavedClip, on WebGL and WebGPU).4. The backends read the capture differently. WebGPU read it through a fixed linear sampler. WebGL used whatever sampler the last draw had left on texture unit 0. Both now read it at the game's texture filter, as a sprite reads its own texture, so a pixel-art game stays crisp on WebGPU as well.
Unchanged: camera effects keep measuring in screen pixels, an effect given a
textureSizekeeps it, and customShaderEffects are never given a size.Example: the sprite example's two chained looks (the hero's Outline + Glow, and the fading red monster) are re-tuned for the new units and look as they did.
Type of change
Checklist
pnpm lintpasses): eslint and biome on the changed files,tscon melonjspnpm testpasses): 365 files, 9127 testspnpm build)Tests:
effect-texture-size.spec.js(WebGL pixels): an outline is equally thick with one effect and through a chain on a scaled sprite, with a different scale on each axis, rotated, under a zoomed camera, and on a default-size container. A game-giventextureSizeis kept. The capture is read at the game's filter, even when the last draw left the other filter's sampler on the unit (tested in both directions).effect-local-space.spec.js: every pattern effect draws the same chained as alone under a zoomed camera, and the chain's space maps the art's corners from the target's pixels through zoom and offset.effect-chain-clip.spec.js(new, WebGL pixels): 0 to 3 effects, a single effect withpostEffectNeedsCapture, and a clipping container all stay inside the clip, and the clip is back as it was afterwards.webgpu_post_effect_flow.spec.js: the clip is resumed right before the composite, after the passes between effects, and never for a camera's chain.webgpu_render_target.spec.jsand a WebGPU mock test: sampling at the game's filter, rebuilt when the filter changes, and the pixel size handed to a chain's pass.restore()that follows reconciles, so it can't change a pixel.Checked in a browser: a temporary demo (not committed) shows each case as pairs, one effect next to the same effect plus a pass-through, including a second camera zoomed ×2. On master the pairs differ and the zoomed camera spills; with the fix they match, on WebGL and WebGPU.
A sweep of the 22 gallery examples that use effects, against master on both backends, found these differences above run-to-run noise:
🤖 Generated with Claude Code
https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx