Capítulo 843 de 859

Chapter 843: Responsive Design (Manual)

Core Idea

Making a three.js app responsive means letting CSS control the canvas's display size while keeping the camera aspect and the renderer's internal resolution (drawing buffer) in sync with that display size — plus deliberate choices around high-DPI displays, where rendering at full device resolution can be far more expensive than it's worth.

Key Concepts

  • CSS controls display size: setting the canvas to width: 100%; height: 100%; display: block (with html/body sized to fill the viewport) lets the canvas resize like any other element — no three.js code changes needed for it to work inside different layouts (sidebars, inline in a document, etc.).
  • Camera aspect must track canvas size: reading canvas.clientWidth/clientHeight each frame and updating camera.aspect (plus updateProjectionMatrix()) prevents the classic "stretched cube" bug.
  • Display size vs. drawing-buffer size: a canvas has a CSS display size and a separate internal pixel resolution; renderer.setSize(width, height, false) sets the internal resolution without letting three.js also override the CSS size (the false argument is important).
  • Resize-check-before-resize pattern: only call setSize when the computed size actually differs from the canvas's current internal size, since browsers treat canvas resizing specially and unnecessary resizes aren't free.
  • HD-DPI trade-off: doing nothing special renders at 1x CSS-pixel resolution and lets the browser upscale — often the right call, since a 3x HD-DPI phone display means 9x the pixels to render, which most mobile GPUs can't sustain.
  • renderer.setPixelRatio() vs. manual scaling: setPixelRatio is convenient but decouples the size you asked for from the size three.js actually uses internally, which causes confusion anywhere you need the real drawing-buffer size (post-processing, gl_FragCoord-based shaders, screenshots, GPU picking); computing devicePixelRatio-scaled dimensions yourself and passing them directly to setSize keeps "the size I asked for" and "the size in use" identical.

Code Examples

function resizeRendererToDisplaySize(renderer, maxPixelCount = 3840 * 2160) {
  const canvas = renderer.domElement;
  const pixelRatio = window.devicePixelRatio;
  let width = Math.floor(canvas.clientWidth * pixelRatio);
  let height = Math.floor(canvas.clientHeight * pixelRatio);

  const pixelCount = width * height;
  const renderScale = pixelCount > maxPixelCount ? Math.sqrt(maxPixelCount / pixelCount) : 1;
  width = Math.floor(width * renderScale);
  height = Math.floor(height * renderScale);

  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) renderer.setSize(width, height, false);
  return needResize;
}
  • What it demonstrates: manually computing a device-pixel-ratio-scaled render size, capped at a maximum pixel budget, instead of relying on setPixelRatio.

Key Takeaways

  1. Let CSS control the canvas's display size (width/height: 100%, display: block) rather than hardcoding pixel dimensions in JS.
  2. Sync camera.aspect to the canvas's CSS size every frame, calling updateProjectionMatrix() after any change.
  3. Use renderer.setSize(width, height, false) — the false prevents three.js from overriding the CSS size you already set.
  4. Prefer computing your own device-pixel-ratio-scaled size (optionally capped by a max pixel budget) over renderer.setPixelRatio(), so the size you requested and the size actually in use never diverge.

Connects To

  • WebGLRenderer: owns setSize/setPixelRatio, the core APIs this chapter is built around.