THREE.JS r187 QUALITY LOOP
You are running a quality loop on a Three.js project. Iteration count is an output, not an input. Never pick a number up front. There is always more you could add. That is why the ceiling must be fixed before pass one, or the loop has no way to tell good from busy.
PROBE (once)
Record:
- three version (pin exact 0.187.0 = r187; allow
@types/three >= 0.186.0 if 0.187.x types lag upstream)
- active pipeline: WebGLRenderer + EffectComposer OR three/webgpu + RenderPipeline/TSL PostProcessing
- WebGL 2 fallback requirement (if three/webgpu is active, record whether forceWebGL: true parity is in scope)
- loop style (manual THREE.Timer step vs rAF), bundler, R3F or vanilla
Grep the project against the 186 -> 187 migration surface (r187 removed all r177 deprecations):
- WebGL passes: subclassed Pass touching removed public quad (now internal _fsQuad)
- WebGPU/TSL: implicit math type coercion regressions, storage buffer binding mismatches
- PMREM (#
34585#): custom shaders, ViewportTextureNode, or env readbacks assuming 2D equirect/cubeUV targets instead of cube render targets
- Depth: manual reversed-depth workarounds (fixed upstream, delete them)
- Manifest: caret or tilde version ranges on three (pin 0.187.0 exact)
CEILING (once, before pass one)
Write down what "highest quality" means for this brief, as numbers:
- GPU frame time budget (ms via timer query) and CPU submission budget (ms) on target device
- draw call, triangle, texture count, and geometry count budgets (via
- perceptual pixel diff threshold (e.g., FLIP or RMSE) below which a change is imperceptible
- the brief's required elements, as a binary checklist
- critic score target for G4
If it is not on this list, it is not part of the ceiling.
GATES (ordered, fail-fast)
G0 Build: imports resolve, zero runtime deprecation warnings, typecheck passes, zero shader/WGSL compile warnings.
G1 Determinism: override Math.random with seeded PRNG and step THREE.Timer manually. Await LoadingManager, KTX2/Draco workers, document.fonts.ready, and renderer.compileAsync(scene, camera) before frame 0. Render two independent runs at fixed frame N: zero NaN/Inf, exact SHA-256 pixel match on software rasterizer (or maxChannelDelta <= 1 across 99.9% of pixels on hardware GPU).
G2 Resource & Lifecycle: GPU frame time (EXT_disjoint_timer_query_webgl2 or WebGPU timestamp-query), CPU frame time, (calls, triangles), and (geometries, textures) inside ceiling budgets. Run lifecycle soak: mount -> render 120 frames -> unmount/dispose -> remount. Assert zero net delta on geometries, textures, and compiled programs.
G3 Rendering:
- Automated: renderer.outputColorSpace === SRGBColorSpace; albedo/emissive maps = SRGBColorSpace; normal/roughness/metalness/AO data maps = NoColorSpace; active tone mapping non-None (unless brief mandates unmapped); shadow map depth buffers non-empty on shadow-casting lights.
- Material policy: enable EON rough diffuse only on hero dielectric assets already budgeted for MeshPhysicalMaterial with roughness greater than 0.5 (never upgrade background MeshStandardMaterial props if it threatens G2).
G4 Intent & Temporal Stability: an independent blind critic receives ONLY a 3-angle static contact sheet, a 3-frame motion burst (t, t+16ms, t+32ms), a temporal luminance-delta heatmap, the G2/G3 metric table, and the brief checklist. It never sees your reasoning or your code diff. Fails immediately on temporal shimmer, z-fighting, or shadow cascade swimming.
EACH PASS
1. Target the lowest failing gate (G_k). Only that one.
2. Isolate one root cause. Fix one mechanism (you may touch any file required by that mechanism).
3. Re-measure G0 through G_k (and G_{k+1}..G4 once G_k passes). Record the full metric vector.
4. Keep the change ONLY IF:
- zero passing lower gates (G0 .. G_{k-1}) regressed, AND
- the targeted sub-metric in G_k improved monotonically (even if a multi-bottleneck gate as a whole has not yet crossed threshold).
Otherwise, git revert immediately.
Renames, comments, dead-code cleanup, and formatting do not count as a pass.
PHASE BUDGETS (decoupled)
Plumbing budget (G0-G1): max 3 passes. If build or determinism fails after 3 passes, halt and escalate harness/environment blockers.
Convergence budget (G2-G4): max 6 passes. Plumbing fixes do not consume convergence passes.
CONVERGENCE vs EXPANSION
Convergence closes the gap to the ceiling. It is finite. This loop does convergence only.
Expansion adds detail that is not in the brief. It is unbounded. It is not a pass.
If you see a detail worth adding, do not add it. Log it as a proposed requirement. After the loop stops, a human decides whether to accept it. Accepted additions become a new brief: update the ceiling, re-run the probe, run the loop again, stop again.
STOP WHEN
Converged: G0-G4 all pass, and for 2 consecutive convergence passes perceptual diff is below the ceiling threshold (or metric delta is inside measurement noise).
Stalled: targeted sub-metric unchanged for 2 consecutive passes. Escalate to structural root-cause analysis. No third retry.
Capped: 3 passes spent in G0-G1, OR 6 passes spent in G2-G4. Past that you are polishing noise or fighting architecture.
REPORT
- stop rule fired and phase (Plumbing vs Convergence)
- full metric and gate vector history per pass
- convergence trajectory: last-pass delta vs remaining gap to ceiling, plus pass-over-pass decay ratio, classified as Asymptotic (parameter tuning exhausted) or Structural (requires architecture change outside loop)
- proposed expansions, each as one line, not implemented