在 3D 场景里做"手指按住时局部放大"的放大镜,最直觉的做法是截取当前画面,再把这张截图用 CSS 放大显示在手指上方。
这个方法在普通 2D 界面里没问题,但放到有透视关系的 3D 场景里,会产生一个根本问题:截图放大只是在放大已经渲染好的像素,并没有重新计算相机投影。近大远小的关系不会随着"视点变近"重新生成,因此放大后的内容更像被拉伸,而不是从更近的位置重新看一遍场景。
我之前在一个测量复制 demo 里遇到过这个问题。触屏端长按并拖动测量项的端点或数字标签时,需要在手指上方显示一个圆形放大镜,把局部场景放大约 1.3 倍,帮助用户判断端点最终落在哪里。
这次的功能范围也提前做了限制:放大镜只重新渲染 Three.js 场景中的模型、线段和端点,不包含数字标签等 HTML 浮层。后者是独立 DOM,如果也要进入放大镜,需要重新复制并映射到另一套局部坐标系,成本明显更高,而当前任务并不依赖它。
最终没有采用截图缩放,而是使用相机的 setViewOffset(),让同一个 Three.js 场景以同一套相机参数重新渲染触摸点附近的一小块区域。
拖拽过程中,每一帧大致做四件事:
setViewOffset(),并更新投影矩阵;scene 和 camera,把这块局部画面渲染到一个约 140 x 140px 的独立小画布;小画布外层再用圆形容器裁切,加白色描边、阴影和中心准星,并整体放在触摸点上方,避开手指遮挡。
这里最关键的是:放大镜里的内容不是主画面的像素副本,而是同一个 3D 场景经过同一套相机投影重新计算后的局部结果。 因此模型边缘、端点和空间关系仍然保持原来的透视逻辑。
第一版虽然方向是对的,但实际效果仍然出现了明显失真。
当时裁剪区域直接按"主画布宽度 ÷ 1.3、主画布高度 ÷ 1.3"计算。主画布是竖屏长方形,因此裁出来的区域也保持了长方形比例;但放大镜的渲染目标是 140 x 140px 的正方形。
结果就是:一个长方形视口被硬塞进正方形渲染目标,端点和纹理被拉伸,拖拽中的端点甚至会糊成一整块纯色圆形。
修正方式很简单:裁剪区域也改成正方形,用主画布宽高中较小的一边作为基准,再按放大倍率计算裁剪尺寸。这样裁剪视口和渲染目标的宽高比一致,模型边缘、端点和纹理细节都恢复正常。
这个问题说明,局部渲染不仅要关心"放大多少",还要保证:源视口的宽高比 = 目标渲染区域的宽高比。
否则即使相机投影本身没有问题,最后一步仍然会因为二维映射关系不一致而变形。
这个放大镜只服务于触屏拖拽,因此没有放进通用输入逻辑里。
触屏操作进入长按拖拽后才创建和显示对应的放大镜资源;鼠标路径不会调用这套逻辑,桌面端也不会额外创建对应 DOM 和 WebGL 渲染目标。
这里的判断不是"每帧检查当前是不是桌面端",而是在输入路径一开始就把功能隔开。对于只服务于某一种输入方式的交互,这样可以减少无关状态和额外资源。
数字标签没有进入放大镜,是一个提前确定的范围取舍。
Three.js 场景内容可以直接通过相机重新渲染,但 HTML 标签位于另一层 DOM 中。如果也要放大,需要额外处理标签复制、局部坐标映射、遮挡和同步更新。
当前用户真正需要确认的是端点与模型的位置关系,而不是读取标签文字,因此没有为了视觉上的"全部都被放大"增加这一层复杂度。
这个范围是在实现前就和需求方确认的。对这类跨 WebGL 与 DOM 的功能来说,先明确哪些内容必须进入效果范围,比做到中途再因为实现成本过高而缩减范围更稳妥。
这次技术方案主要由 Claude Code 提供,我负责确认它是否符合实际设计目标,以及在真机上验证最终效果。
真正暴露问题的不是方向选择,而是具体比例。第一版已经用了正确的局部渲染方案,但裁剪框和目标画布宽高比不一致,依然导致明显失真。
这类 3D UI 问题很适合用可运行原型验证:技术路径可以先判断是否合理,但尺寸、比例和实际触屏视觉效果,仍然需要让它真正跑起来再确认。