首页 / Three.js 入门教程 / 响应式设计

Three.js 入门教程

响应式设计

本教程共 40 篇 · 第 8 篇 · 更新于 2026-08-14 · 约 7 分钟阅读

Three.js响应式resize像素比抗锯齿canvasResizeObserver

本节目标:让 three.js 画面在窗口缩放、设备旋转、高清屏上不变形、不模糊,学会 resize 事件、像素比适配和抗锯齿。

窗口变了,画面为什么变形

canvas 的尺寸是创建时固定的。窗口变宽后,canvas 没跟着变,画面要么被拉伸,要么留白,要么被裁掉。

要让画面自适应,需要同时更新三样东西:

  1. canvas 的显示尺寸,由渲染器负责。
  2. 相机的宽高比 aspect
  3. 相机的投影矩阵。

缺了任何一步,画面都会不正常。下面逐个说。

先解释一个常见的疑问:为什么不能只用 CSS 把 canvas 拉大?因为 canvas 的绘图缓冲区是在 setSize 时按像素分配的,CSS 拉伸只是把已画好的图放大或缩小,结果要么模糊,要么变形。正确的做法是让缓冲区、显示尺寸、相机比例三者保持一致。

更新相机:aspect 与投影矩阵

宽高比变了,透视相机的画面比例就得跟着变:

camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();

updateProjectionMatrix() 用新的 aspect 重新计算投影矩阵。改了 aspect 不调用它,是最常见的坑:相机数据是新的,投影矩阵还是旧的,画面比例一直不对。

更新渲染器:setSize

renderer.setSize(container.clientWidth, container.clientHeight);

setSize 同时设置 canvas 的样式尺寸和内部缓冲区尺寸。样式尺寸决定显示多大,缓冲区尺寸决定画多清晰。

它还有一个可选参数 updateStyle,默认 true。如果 canvas 的尺寸完全由 CSS 控制,可以传 false,让 setSize 只管缓冲区:

renderer.setSize(w, h, false); // 不碰 CSS 样式

也就是说 canvas 有两个尺寸概念:显示尺寸(CSS)和缓冲区分辨率(绘图表面)。setSize 设置两者,setPixelRatio 控制两者的倍数关系。显示器像素密度越高,缓冲区分辨率就要越高,画面才不糊。

监听 resize 事件

窗口尺寸变化时,window 会触发 resize 事件:

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

手机横竖屏切换、拖拽窗口、把窗口拖到另一块显示器,都会触发这个事件。一段代码全部覆盖。

拖拽窗口时 resize 会连续触发,但回调里只有两三行赋值,开销极小,一般不需要防抖。如果回调里塞了重活(比如重新加载纹理),再考虑加防抖。

更好的做法是把更新逻辑抽成函数,初始化和事件回调共用一份代码,保证两处逻辑一致:

function updateSize() {
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
}
updateSize(); // 初始化
window.addEventListener('resize', updateSize);

高清屏:像素比

window.devicePixelRatio 是设备像素比。Retina 屏是 2,意思是 1 个 CSS 像素对应 2×2 个物理像素。

不处理像素比,高清屏上的画面会发虚:canvas 只有 CSS 尺寸对应的物理像素,屏幕却要用更多像素去显示。

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

Math.min(dpr, 2) 把像素比封顶在 2。3 倍屏的清晰度收益肉眼难辨,但像素数量是 9 倍,渲染开销大很多,移动端尤其明显。

还有一个性能点:setSize 的尺寸越大,每帧要填充的像素越多。全屏 + 像素比 2,就是 4 倍像素量。场景复杂、帧率不够时,适当降低像素比是立竿见影的优化手段。

Note

setPixelRatiosetSize 配合使用:像素比决定内部画多清晰,尺寸决定显示多大。只设尺寸不设像素比,高清屏上就是糊的。

抗锯齿:消灭锯齿边

斜线在像素网格上会呈现阶梯状,这叫锯齿(jaggies)。渲染器默认不开抗锯齿,物体边缘会比较毛糙。

const renderer = new THREE.WebGLRenderer({ antialias: true });

antialias 必须在创建渲染器时设置,创建之后改不了。它走 GPU 的多重采样(MSAA),现代浏览器基本都支持,对性能影响不大。

Note

想改 antialias,只能重建一个渲染器。创建前想清楚,大部分场景直接开 true 就行。
移动端性能紧张时,可以考虑关掉抗锯齿换帧率——锯齿通常比掉帧容易接受。

完整示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>8. 响应式设计</title>
  <style>
    * { margin: 0; padding: 0; }
    html, body { height: 100%; overflow: hidden; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
  import * as THREE from 'three';

  // 用容器尺寸初始化,而不是写死数字
  const container = document.body;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a2e);

  const camera = new THREE.PerspectiveCamera(
    50,
    container.clientWidth / container.clientHeight,
    0.1, 100
  );
  camera.position.set(3, 3, 5);
  camera.lookAt(0, 0, 0);

  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1.5, 1.5, 1.5),
    new THREE.MeshBasicMaterial({ color: 0x63e6be })
  );
  scene.add(cube);

  // 抗锯齿必须在创建时开启
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(container.clientWidth, container.clientHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  container.append(renderer.domElement);

  // 窗口尺寸变化:更新相机宽高比 + 投影矩阵 + 渲染器尺寸
  window.addEventListener('resize', () => {
    camera.aspect = container.clientWidth / container.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(container.clientWidth, container.clientHeight);
  });

  // 动画循环
  const clock = new THREE.Clock();
  function animate() {
    requestAnimationFrame(animate);
    const delta = clock.getDelta();
    cube.rotation.x += 0.6 * delta;
    cube.rotation.y += 0.9 * delta;
    renderer.render(scene, camera);
  }
  animate();
  </script>
</body>
</html>

运行后拖拽窗口边缘,或者按 F12 打开开发者工具的设备模拟器旋转屏幕。方块始终居中,比例不变,边缘平滑。

别忘了 HTML 里的 <meta name="viewport">。没有它,手机浏览器会把页面按桌面宽度渲染再缩小,尺寸计算全乱套。

还有一个边缘情况:把窗口从普通屏拖到 Retina 屏,devicePixelRatio 会变化。严谨的做法是在 resize 回调里重新设置像素比:

window.addEventListener('resize', () => {
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  // 其他更新:aspect、updateProjectionMatrix、setSize
});

大多数场景不处理也没问题,但常接多显示器的机器值得加上。

Tip

clientWidth / clientHeight 而不是 innerWidth / innerHeight,代码就能同时适配全屏和局部容器,换到 React、Vue 项目里也能直接用。

容器化:ResizeObserver

监听 window.resize 只能覆盖窗口变化。如果 three.js 画布只是页面的一部分(比如侧边栏、某个卡片),应该监听容器元素。浏览器原生的 ResizeObserver 就是干这个的:

const observer = new ResizeObserver(() => {
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
});
observer.observe(container); // 容器尺寸一变,回调就执行

它比 window.resize 精确:容器尺寸变化就触发,与窗口无关。布局稳定后,这是更推荐的方案。

最后列几个常见问题和排查顺序:

  • 画面模糊:没设 setPixelRatio,或设了但尺寸没跟着变。
  • 画面拉伸变形:aspect 没更新,或忘了 updateProjectionMatrix()
  • 画面空白或尺寸为 0:容器布局还没完成就初始化了 three.js,等布局完成后再取 clientWidth

响应式三件套(aspect、投影矩阵、setSize)记牢,这类问题几分钟就能定位。