Stop the outline breathing during camera moves (#121), and the primitive fitter re-sampling the mesh field every trial (#123) - #140
Open
kmatzen wants to merge 2 commits into
Open
Conversation
…hen the camera moves The outline kernel was a fixed count of depth-texture texels, and ThreeEngine drops the device-pixel ratio from 1.5 to 1.0 for the duration of a camera drag. A texel is a different size on screen at each ratio, so the outline rendered 1.5x wider while moving and snapped back ~180ms after release — most visible on a Retina display against a dark background (#121). Turn the shaders' kernel radius back into a uniform and drive it by the current pixel ratio: u_radius = cssRadius * pixelRatio. Pinned to the idle-ratio width (2.0 CSS px for the shape outline, ~1.667 for the gizmo), the on-screen thickness is now constant across the interactive/idle ratio switch. R stays a compile-time 3, and the CSS radius times the 1.5 ceiling lands exactly on it, so the loop is never overrun; a clamp guards the bound if MAX_DPR is ever raised. The GIZMO_OUTLINE_FRAG kernel had the same texel-measured drift and gets the same treatment. outlineTexelRadius is a pure function with a unit test covering CSS-width constancy across the two ratios, the preserved idle appearance, and the clamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToZMaeRwS2FKB8xESMuktQ
|
🚀 Preview deployed: https://claude-existing-prs-issues-s.sinter.pages.dev (updates on every push to this PR) |
fitPrimitive ran ~2-3s per fit here (5-9s on a contended machine), and the cost did not fall with grid resolution — it was dominated by a fixed-iteration coordinate descent rather than by the field's size. It is also the suite's chronic timeout: the test file runs ten fits and repeatedly ran out the clock. volumeCost is called thousands of times per fit — every coordinate-descent trial of every candidate — always over the same fixed sample grid, with only the candidate node changing. Each call re-sampled the mesh field (sampleMeshField: a trilinear interpolation with a square root) at those identical coordinates and allocated a fresh [x,y,z] tuple per sample, so a single fit ran to millions of redundant interpolations and short-lived arrays. Precompute the sample coordinates and the mesh-field target once per fit (sampleVolume), and have volumeCost read the target from a Float64Array and evaluate the candidate with evalAt's loose scalars — no re-interpolation, no per-sample allocation. The sample order is unchanged, so the summation is bit-identical and the fit stays deterministic. Measured (res 40): sphere 2638->342ms, box 2352->298ms, cylinder 2255->284ms, tilted cylinder 3085->400ms — ~8x, with kind/surfaceRms/surfaceMax identical. fitPrimitive.test.ts drops from tens of seconds to ~17s, now bounded by field baking rather than fitting. All existing gates pass, determinism included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToZMaeRwS2FKB8xESMuktQ
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.
Two independent fixes on this branch.
1. Silhouette outline breathes while the camera moves — fixes #121
Orbit, pan or zoom and the white silhouette outline visibly thickened (~1.5×) for the duration of the drag, snapping back ~180 ms after release. Most obvious on a Retina display against a dark background; invisible on a 1× display, which is why it survived.
Why.
ThreeEnginedrops the device-pixel ratio while the camera moves —1.5idle,1.0interactive (a deliberate perceived-latency win).OUTLINE_FRAGmeasured its kernel in texels of the depth texture (const float u_radius = 3.0), and 3 texels is 2.0 CSS px at ratio 1.5 but 3.0 CSS px at ratio 1.0 — so the outline was 1.5× wider on screen exactly while moving.Fix. Turn the kernel radius back into a uniform in both shaders and drive it by the current pixel ratio each frame:
u_radius = cssRadius × pixelRatio. Pinned to the idle-ratio width (2.0 CSS px for the shape outline, ~1.667 for the gizmo), on-screen thickness is now constant across the switch.Rstays a compile-time3;cssRadius × 1.5lands exactly on it, so the loop is never overrun. AMath.minclamp guards the bound ifMAX_DPRis ever raised.outline / total), so opacity is preserved — only the width scales.GIZMO_OUTLINE_FRAGhad the same drift (r > 2.5) and gets the same treatment.outlineTexelRadiusis a pure function with a GPU-free unit test (OutlinePass.test.ts) covering CSS-width constancy across the two ratios, the preserved idle appearance, linear scaling, and the clamp.2.
fitPrimitivecosts seconds per fit, independent of grid resolution — fixes #123A single fit took ~2–3 s here (5–9 s on a contended machine) and did not fall with grid resolution — it was dominated by a fixed-iteration coordinate descent, not the field size. It is also the suite's chronic timeout:
fitPrimitive.test.tsruns ten fits and repeatedly ran out the clock.Why.
volumeCostis called thousands of times per fit — every coordinate-descent trial of every candidate — always over the same fixed sample grid, with only the candidate node changing. Each call re-sampled the mesh field (sampleMeshField, a trilinear interpolation with a square root) at those identical coordinates and allocated a fresh[x,y,z]tuple per sample. A single fit ran to millions of redundant interpolations and short-lived arrays.Fix. Precompute the sample coordinates and the mesh-field target once per fit (
sampleVolume);volumeCostreads the target from aFloat64Arrayand evaluates the candidate withevalAt's loose scalars — no re-interpolation, no per-sample allocation. The sample order is unchanged, so the summation is bit-identical and the fit stays deterministic.Measured (res 40), before → after: sphere
2638 → 342 ms, box2352 → 298 ms, cylinder2255 → 284 ms, tilted cylinder3085 → 400 ms— ~8×, withkind/surfaceRms/surfaceMaxidentical.fitPrimitive.test.tsdrops from tens of seconds to ~17 s, now bounded by field baking rather than fitting.The existing gates (recover sphere/box/cylinder, refuse non-primitives with a reported residual, tilted-axis recovery, and
is deterministic) are exactly the invariants #123 asks any speedup to keep; no timing test, per the repo's stated "nothing asserts elapsed time" policy.tsc --noEmitclean; full unit suite 557 passed / 3 skipped for both changes.🤖 Generated with Claude Code
https://claude.ai/code/session_01ToZMaeRwS2FKB8xESMuktQ