VR Web 应用中的字体渲染问题——位图 vs. SDF

原作者:HorstW
原始链接:https://discourse.threejs.org/t/vr-web-applications-font-rendering-issues-bitmap-vs-sdf/93545


背景与问题

几年前,作者在为 Splunk 制作 VR 演示时,萌生了利用 VR 无限屏幕空间来承载复杂业务应用的想法。他曾在 Medium 上撰写系列文章(首篇为 Enterprise VR: Will it happen?),但直到 2024 年才在 Quest 3 设备上认真实践这些概念。

作者为交互式 2D 界面设计的方案是:基于 three.js 构建一个框架,把界面渲染到 Canvas 纹理 上,并在其上模拟类似 React 的组件模型(但远比 DOM 轻量)。参考应用是科幻小说配套应用 Veer Index。

然而,几乎让他放弃整个方案的问题是:在 Quest 3 和 Galaxy XR 上,WebVR 中的字体非常模糊。SDF 字体渲染(他尝试了 troika)看似是显而易见的解决方案,但实际效果并不理想:

  • 使用 troika 渲染的文本在远处产生刺眼的闪烁(shimmer),而且实际清晰度提升有限。
  • 从 Canvas API 全面转向 100% 基于着色器(shader-based)的渲染 来实现视觉丰富的 2D 内容,会带来大量复杂性。

关键发现与解决方案

作者最终找到的突破口来自一个观察:Virtual Desktop 本质上只是一个流式传输的位图,但它的字体渲染却相当清晰。

进一步排查发现:three.js 的 WebXR 渲染并没有使用设备的完整原生分辨率(至少在 Quest 3 上如此)。也就是说,WebXR 默认的帧缓冲缩放因子(framebuffer scale factor)低于设备面板的实际分辨率,导致 Canvas 纹理被低分辨率采样后放大,字体因此模糊。

解决方法是手动“升级”渲染分辨率:

renderer.setFramebufferScaleFactor(FRAMEBUFFER_SCALE);

更关键的是,XRWebGLLayer.getNativeFramebufferScaleFactor() 返回的值是 1,但作者通过实际测试发现这个值是错误的。他经验性地确定至少需要:

const FRAMEBUFFER_SCALE = 1.35;

才能获得预期的字体清晰度。

这一发现成为作者在空间 Web 应用领域的转折点。此前他几乎要放弃该方向。


核心优化要点

  1. 在 WebXR 中手动设置帧缓冲缩放因子,强制使用更高分辨率渲染
    默认情况下,three.js 在 WebXR 中可能未使用设备全原生分辨率,导致 Canvas 位图纹理被低分辨率采样。通过 renderer.setFramebufferScaleFactor(FRAMEBUFFER_SCALE) 可以提高渲染目标分辨率,显著改善字体清晰度。在 Quest 3 上,经验值至少为 1.35。

  2. 不要盲目相信 XRWebGLLayer.getNativeFramebufferScaleFactor() 的返回值
    该 API 在部分设备(如 Quest 3)上返回 1,但实际并非如此。开发者应在目标设备上实测不同缩放因子,找到清晰度与性能之间的最佳平衡点。

  3. 对于 2D 界面,基于 Canvas 的位图渲染在 VR 中可能比纯 SDF 着色器渲染更实用
    虽然 SDF(有向距离场)字体渲染在理论上能提供平滑缩放,但在 VR 场景中会引入远处闪烁问题,且完全用着色器实现复杂 2D 内容成本过高。通过提高帧缓冲缩放因子,Canvas 位图方案能够达到足够清晰度,同时保留开发效率。


GLSL/WebGL 专业术语检查

以下是文中涉及的相关术语及准确翻译/说明:

术语翻译/说明
SDF (Signed Distance Field)有向距离场(或符号距离场),一种基于距离函数的纹理表示,常用于字体渲染以实现平滑缩放。
Shader-based rendering基于着色器的渲染,即使用 GLSL/着色器直接绘制内容,而非通过 Canvas 2D API 生成位图。
Canvas APIHTML5 2D 绘图接口,用于生成位图纹理。
Bitmap位图,基于像素的纹理图像。
Framebuffer帧缓冲,渲染目标缓冲区。
Framebuffer Scale Factor帧缓冲缩放因子,决定渲染目标分辨率与设备原生分辨率之间的比例。
WebXRWeb 扩展现实 API,用于在浏览器中实现 VR/AR 体验。
XRWebGLLayerWebXR 中管理 WebGL 渲染层的接口。
getNativeFramebufferScaleFactor()XRWebGLLayer 的方法,用于获取设备原生帧缓冲缩放因子(本文指出其在 Quest 3 上返回值不可靠)。
renderer.setFramebufferScaleFactor()three.js WebGLRenderer 的方法,用于设置 WebXR 渲染时的帧缓冲缩放因子。
Shimmer / Aliasing闪烁/锯齿,指远处高频细节(如 SDF 边缘)在采样不足时产生的视觉伪影。
Troika一个 three.js 的文本渲染库,支持 SDF 字体渲染。

以上术语在翻译和使用时需保持准确,避免混淆。


核心代码保留

// 手动设置 WebXR 帧缓冲缩放因子,提高渲染分辨率
renderer.setFramebufferScaleFactor(FRAMEBUFFER_SCALE);

// 实测经验值:在 Quest 3 上至少需要 1.35 才能获得清晰字体
const FRAMEBUFFER_SCALE = 1.35;

// 注意:该 API 可能返回错误值(如 1),需实测校准
XRWebGLLayer.getNativeFramebufferScaleFactor();

总结

HorstW 通过一个看似简单但常被忽略的细节——WebXR 帧缓冲缩放因子——解决了 VR 应用中字体模糊的难题。这一经验对于任何在 three.js 中构建 VR 2D 界面的开发者都具有重要参考价值。在空间计算时代,细节决定体验,而手动校准渲染分辨率正是其中关键一环。

Happy hacking!
—— Horst