@@ -508,6 +508,16 @@ export class FastModelPicker implements Disposable {
508508 // scene with lots of overdraw — collapses to ~O(SCISSOR_PX²).
509509 // 4×4 leaves a 1-pixel margin around the read site to absorb any
510510 // sub-pixel rounding in NDC→pixel conversion without missing.
511+ //
512+ // DPR caveat: `WebGLRenderer.setScissor` always multiplies its
513+ // arguments by the canvas pixelRatio before forwarding to GL,
514+ // even when the active target is an offscreen FBO at a different
515+ // resolution. The picker's FBO is sized in CSS pixels via
516+ // `renderer.getSize()`, so on a hi-DPI display passing FBO-pixel
517+ // coordinates straight through lands the GL scissor outside the
518+ // FBO — every read pixel comes back zero. Pre-divide by the
519+ // pixel ratio so three's multiplication restores the FBO-pixel
520+ // coords we want.
511521 const SCISSOR_PX = 4 ;
512522 const tw = target . width ;
513523 const th = target . height ;
@@ -518,7 +528,13 @@ export class FastModelPicker implements Disposable {
518528 const half = SCISSOR_PX >> 1 ;
519529 const sx = Math . max ( 0 , Math . min ( tw - SCISSOR_PX , cx - half ) ) ;
520530 const sy = Math . max ( 0 , Math . min ( th - SCISSOR_PX , cy - half ) ) ;
521- renderer . setScissor ( sx , sy , SCISSOR_PX , SCISSOR_PX ) ;
531+ const dpr = renderer . getPixelRatio ( ) ;
532+ renderer . setScissor (
533+ sx / dpr ,
534+ sy / dpr ,
535+ SCISSOR_PX / dpr ,
536+ SCISSOR_PX / dpr ,
537+ ) ;
522538 renderer . setScissorTest ( true ) ;
523539
524540 const objectsByModel = new Map < string , THREE . Object3D > ( ) ;
0 commit comments