Adding and updating filter logic to match Air/Flash - #10
Merged
Conversation
OpenFL's blur is a fixed 7-tap Gaussian stacked round(blur*quality/4)+1 times at halving radii -- a heuristic that doesn't match Flash: too soft at low quality, too tight/undersampled at large radii (visible faceting). Add a fractional separable box blur (Ruffle's algorithm, faithful to Flash Player) behind -Dflash_box_blur, keeping the Gaussian as the default so the two can be A/B'd. Per axis, per pass: full_size = blur (<=255); radius = (full_size-1)/2; m = ceil(radius)-1 interior double-weighted bilinear pairs; alpha = fractional edge weight quantised to 8 bits (imitating Flash's fixed point); first/last edge taps weighted alpha and alpha+1; normalise by full_size; 8-bit round each pass. Passes = quality*2 (one H + one V per quality iteration; separable box passes commute). Shader loop is constant-bounded (64 pairs) with a dynamic break for WebGL1 compatibility. Verified on the filter grid vs the Flash/AIR reference: box blur matches Flash's crispness at quality 1 and its directional/wide spread at quality 3 and asymmetric radii, where the Gaussian did not. Glow/shadow/ bevel are unaffected (they use their own internal blur). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The box blur added to BlurFilter only fixed the standalone blur. Glow, drop shadow and bevel each have their OWN copy of the old too-tight Gaussian heuristic (round(blur*quality/4)+1 passes of a 6/7-tap Gaussian at halving radii), so their blur did not match Flash: shadows read weaker/tighter than the reference and bevels were soft/diffuse (looked like a resolution loss), in both single and stacked use, on every target. Extend the box blur to all three, behind the same -Dflash_box_blur gate: - BlurFilter: extract __setupBoxBlur(horizontal, v) static helper. - GlowFilter: BoxBlurAlphaShader (fractional box on the alpha channel, same colourise+strength as BlurAlphaShader) + __setupBoxBlur helper; passes = quality*2. Shared by DropShadowFilter (glow-with-offset). - BevelFilter: reuse BlurFilter.__setupBoxBlur; passes = quality*2. Default (Gaussian) path unchanged. Verified on the grid vs Flash/AIR: bevels are now crisp (matching Flash), and drop/inner shadows spread correctly, single and stacked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GradientGlowFilter did not exist in OpenFL (in the Flash IDE menu; the SWF parser flagged it "not supported on native"). No Ruffle reference — even Ruffle's GPU backend leaves it unimplemented — so matched against Flash/AIR empirically. Implementation (GL shader path): blur the object's alpha into a soft distance field (reusing BlurFilter's box blur under -Dflash_box_blur, or the Gaussian otherwise), build a 256-entry ramp from (colors, alphas, ratios) with ratio 0 = outer / 255 = inner, then a combine shader indexes the ramp by the field and composites per type: outer = ramp*(1-src.a) over src; inner = ramp masked by src.a over src; full = ramp over src everywhere; knockout drops src. distance/angle offset the field like a drop shadow. Falls back to the flash.filters typedef on the flash target. Verified vs Flash/AIR on a magenta->yellow->cyan gradient: outer, inner, full, knockout, fade-out alpha, distance offset and strength all match. Software __applyFilter is a no-op placeholder (GL path is what renders on-screen). Registered via the API; SWF-tag wiring is separate/TODO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The second missing gradient filter (in the Flash IDE menu; SWF parser flagged "not supported on native"; no Ruffle reference). Matched against Flash/AIR empirically. Implementation (GL shader path): blur the object's alpha (reusing BlurFilter's box blur under -Dflash_box_blur, else the Gaussian), then a shader samples the blurred field at +/- the bevel offset (distance,angle) to form a signed distance field (blurLeft - blurRight)*strength, mapped into a 256-entry ramp (ratio 0 = one edge, 128 = base/transparent, 255 = other edge) built from (colors, alphas, ratios). Composites per type inner/outer/full with knockout, same math as BevelFilter but the ramp replaces the highlight/shadow colours. Falls back to the flash.filters typedef on the flash target. Verified vs Flash/AIR on a blue->transparent->white ramp: inner, outer, full and strength all match. Software __applyFilter is a placeholder (GL path renders on-screen). API-registered; SWF-tag wiring is separate/TODO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At high quality the box-blur path (-Dflash_box_blur) applies `quality` passes, each widening the shadow/glow/bevel support by ~half the blur, so the effective reach is ~quality*blur/2 per side. __updateSize() only reserved one blur radius, so the effect was hard-clipped to a rectangle at the cache texture bounds (e.g. quality=15, blur=16) while Flash/AIR radiate smoothly. Reserve ceil(blur*0.5*quality)+4 per side under #if flash_box_blur across DropShadow / Glow / Bevel / GradientGlow / GradientBevel; the Gaussian #else path keeps its original formula for A/B testing. Also make set_quality on DropShadow/Glow/Bevel recompute the extension (update __quality then call __updateSize) so post-construction quality changes resize correctly. Verified against the Flash/AIR baseline via a parameter sweep: the quality cell now matches, and strength/distance/blur/bevel+DS all track the baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gradient bevel's coloured band rendered ~59% wider than Flash/AIR (per-cell mean|diff| ~17 on outer/full/strength). Cause: because the ramp's middle stop is opaque, flat regions map to the middle colour, so the visible band edge is the cache-texture boundary -- and __updateSize reserved ceil(blur*0.5*q + distance) + 4, whose +4 safety margin showed up as extra opaque fill past where Flash draws. Flash's band stops exactly at the box-blur support (quality*blur/2) plus the transform offset (distance) -- the extent where the field bL-bR is non-zero (verified by scanline: FLASH edge at 11px = 8 + 2.83). Set the extension to that true extent, mirroring BevelFilter's asymmetric offset structure but without the +4 margin (BevelFilter can afford it because its band fades to transparent; the gradient band is opaque). Offset magnitude is ceil(abs(distance*cos/sin)) so negative angles don't lose a pixel. Removes the magic +4; adds no new constant (q*blur/2 is the box-blur support, per the Change-5 derivation). Verified vs AIR: band start 199->199, outer-band yellow area 4121->2596 px (= Flash 2596), mean|diff| 17->~1 across inner / outer / full / angle / strength. GradientGlow left as-is (already within tolerance: cyan centre pixel-identical, only sub-pixel transition stretch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The box blur has been validated against the AIR baseline across the full parameter sweep (strength/distance/blur/quality, bevel/glow/gradient/stacks), so the -Dflash_box_blur conditional-compilation gate is no longer needed. This removes the flag from all six blur-based filters (Blur, Glow, DropShadow, Bevel, GradientGlow, GradientBevel): the box-blur branch becomes the only path and the old fixed-tap Gaussian #else branches are deleted, along with the now -dead shader classes BlurShader (BlurFilter) and BlurAlphaShader (GlowFilter). Filters now render correctly with a plain build, no define required. Verified: the flag-free build is pixel-identical to the previous -Dflash_box_blur build (mean|diff| = 0.0) and matches AIR in the same band as before. Net -326 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename for clarity, no behaviour change: - BlurFilter.__setupBoxBlur -> __setupBlurShader (configures BoxBlurShader) - GlowFilter.__setupBoxBlur -> __setupBlurAlphaShader (configures BoxBlurAlphaShader) - GlowFilter.__boxBlurAlphaShader field -> __blurAlphaShader (symmetric with BlurFilter's existing __blurShader field) Callers updated accordingly: Blur/Bevel/GradientGlow/GradientBevel use the colour shader helper; Glow/DropShadow use the alpha shader helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…form) Swap the implementation of BoxBlurShader and BoxBlurAlphaShader for a straight per-texel fractional box: a box of width uFullSize (= the blur amount) built as (2n+1) full-weight interior texels + one fractional-weight texel per edge, divided by the width, 8-bit rounded per pass. The class/field/method names are unchanged; all the box math now lives in the shader, so __setupBlurShader / __setupBlurAlphaShader only pass the axis, width, colour and strength. Weights are identical to the previous Ruffle-derived box, so output matches Flash/AIR to within 8-bit rounding (verified across the blur sweep and every blur-based filter: bevel, glow, gradient, stacks -- all unchanged). It also stays correct above blur 127, where the old shader's fixed 64-tap interior loop truncated the box. Trade-off: ~2x the texture fetches at high blur (no fused bilinear pairs) -- correctness over speed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drops the redundant "box" from the field name, mirroring GlowFilter's __blurAlphaShader. Pure rename, no behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CPU path (BitmapData.applyFilter, BitmapData.draw of a filtered object, software surfaces) diverged badly from both the GL path and Flash. Measured against AIR with a BitmapData.applyFilter harness, which reaches __applyFilter directly on every target and is Flash's own implementation on AIR. Four defects, all fixed: - BitmapData.applyFilter ignored its sourceBitmapData argument: it validated the parameter, then filtered `this`. The documented usage produced an empty bitmap. - Glow/DropShadow had no inner, no knockout, and ignored strength (lime's ImageDataUtil.gaussianBlur accepts strength/color but never uses them). - Bevel only blurred -- no highlight/shadow derivation or composite at all. - Both gradient filters were no-ops. Structural cause for the last three: the GL path hands the unfiltered object to its combine shader, which composites inner/knockout/full itself, while the software path relied on the caller drawing the object back on top -- which can only express "outer, non-knockout". Filters now composite inside __applyFilter and set the new BitmapFilter.__softwareComposite so the two software call sites skip that draw. The GL path never reads the flag and is unchanged. Shared CPU helpers on BitmapFilter mirror the shaders: __alphaField, __blurField (the same fractional box blur as BoxBlurShader) and __compositeEffect (the combine formulas). Each filter builds its effect layer as its shader does -- including inverting the alpha before blurring for inner glow/shadow, matching InvertAlphaShader. BlurFilter's software path is left alone: its StackBlur already matches Flash (0.23). Verified vs AIR across 12 cases: overall mean|diff| 14.95 -> 1.32, every case now below 1.0. GPU render unchanged (diff 0.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a HiDPI display every blur-based filter rendered 1/pixelRatio too small relative to its content -- 0.67x at 1.5x DPI, 0.5x on a 2x panel. Measured against AIR, the outward reach of glow/blur/bevel was a consistent 0.64-0.67x. A filtered object is cached into a bitmap that is correctly sized and drawn at pixelRatio, but the filter's distances were never scaled to match. The shaders work in texels (direction = uDir / uTextureSize, uFullSize = blurX), so a blur of 12 covered 12 device pixels = 8 logical pixels at 1.5x. This predates the box blur: develop's Gaussian had the same flaw (vec2 r = uRadius / uTextureSize), and no filter referenced pixelRatio at all. Add BitmapFilter.__renderScale (default 1), set by the renderer to pixelRatio before each filter is used, on both the GPU and software cache loops. Every filter now scales its blur radii and its offsets/transforms by it, in both __initShader and __applyFilter. BitmapData.applyFilter leaves it at 1 -- it works on the bitmap's own pixels -- so that path is unaffected. Also: BevelFilter.__updateTransform() was only called from the distance/angle setters, i.e. at construction when the render scale is still 1, so scaling it there never took effect; it is now also called from __initShader. Verified vs AIR on a twelve-case harness: GPU mean|diff| 2.06 -> 0.36 (now better than the software path), bevel band width exactly AIR's, software column unchanged at 0.51. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
__updateCacheBitmap rounded the cache origin (offsetX/offsetY) to a whole LOGICAL pixel. At a fractional pixelRatio (1.5x, 1.25x) that is a fractional DEVICE pixel, so the entire cache bitmap was composited to screen at a half-pixel phase and bilinearly smeared. Soft effects hide it; a thin high-contrast band -- a bevel edge -- shows it as a visible displacement. Found via gradientBevel: its extension of 11 logical px put the content at 16.5 device px, and a sub-pixel shift search showed the band displaced by ~0.75px diagonally, recoverable to 0.19 by translation alone. BevelFilter escaped only because its extension is always even (+4), and even x 1.5 is always an integer. Round the origin in device space instead (ceil/floor of rect.x * pixelRatio, then divide back). At pixelRatio 1 this is byte-identical to before; at 1.5x it aligns both the content-into-cache and cache-onto-screen placements. It never under-reserves: floor on the negative left offset only ever adds room. Verified vs AIR on the twelve-case three-way harness: gradientBevel 1.23 -> 0.15 with zero residual shift, every other case unchanged to 2 dp, software column unchanged, GPU average 0.36 -> 0.27. Its opaque-middle area now matches Flash to within 2 px (24294 vs 24292), which also retires the earlier "extent 1px short" observation -- that was the same smear. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
__alphaField -> __alphaMask, __blurField -> __blurMask, __blurFieldAxis -> __blurMaskAxis, __fieldAt -> __maskAt, and the local variables to match. "Field" was scalar-field jargon that needed explaining; "mask" is what most readers expect a blurred single-channel alpha array to be called. The helpers header now says what the array represents: the shape's alpha, one Float per pixel, which blurred becomes the soft coverage map every effect is derived from. The shader comments that say "distance field" are left as-is: there the word names the signed-distance concept the bevel maths actually uses. Pure rename, no behaviour change; builds clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BevelFilter.__applyFilter: hr/hg/hb and sr/sg/sb become highlightR/G/B and shadowR/G/B (each premultiplied by its alpha, as uLightColor/uShadowColor). bL/bR (inherited "blur left/right" from BevelShader) become maskTowardShadow/maskTowardLight in BevelFilter and GradientBevelFilter (CPU path and the GradientBevelShader GLSL): the samples are taken along the light angle, +offset the way a shadow falls, -offset toward the light, so left/right was misleading for any angle other than 0. Comments explain the sign of the difference (positive = edge facing the light = highlight). Pure rename, no behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
No description provided.