Three.js 怎么实现响应式布局以适应窗口大小变化
详细讲解在 Three.js 中如何正确监听窗口和容器尺寸变化,同步更新透视相机宽高比与投影矩阵,调整渲染器输出尺寸与设备像素比,彻底解决 3D 场景拉伸变形与黑边问题。
在默认情况下,Three.js 创建的 <canvas> 画布并不会随浏览器窗口变化而自动伸缩。当用户缩放窗口、旋转移动端屏幕或折叠侧边栏时,画面往往会出现两种异常:一是 3D 物体被硬生生横向或纵向拉伸变形,二是画布周围出现空白黑边或滚动条。实现 Three.js 响应式布局的核心,是在容器尺寸发生变化时,同步更新相机的视锥体宽高比(aspect)与投影矩阵,并重新设定渲染器的像素尺寸与设备像素比。本文将针对全屏与局部容器两种场景,提供可直接落地的完整代码与原理剖析。
一、全屏场景下的标准实现方案
全屏应用是最常见的 Three.js 展示形态。在此类场景中,画布占满整个浏览器视口。让全屏画面自动适应窗口变化,需要依次完成三个动作:绑定 resize 事件、更新透视相机参数,以及同步修改渲染器的像素尺寸。
1. 完整实现代码
以下是全屏模式下完整的响应式实现范例。代码涵盖场景创建、相机、渲染器及窗口监听回调函数:
import * as THREE from 'three';
// 1. 初始化场景与渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 适配高清屏幕像素比
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// 添加示例几何体
const geometry = new THREE.SphereGeometry(1.5, 32, 16);
const material = new THREE.MeshNormalMaterial({ wireframe: true });
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
// 2. 核心响应式监听函数
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;
// 更新透视相机宽高比
camera.aspect = width / height;
// 重新计算并应用相机投影矩阵(至关重要)
camera.updateProjectionMatrix();
// 更新渲染器绘图缓冲区与 Canvas 像素尺寸
renderer.setSize(width, height);
// 重新校准设备像素比(例如跨显示器拖拽窗口时)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 如果不是持续 requestAnimationFrame 循环,需在此处手动 render
// renderer.render(scene, camera);
}
// 绑定全局窗口 resize 事件
window.addEventListener('resize', onWindowResize);
// 3. 动画循环
function animate() {
requestAnimationFrame(animate);
sphere.rotation.x += 0.005;
sphere.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
<canvas> 是行内元素(inline),容易受到行高影响产生 3~5px 的底部空隙,导致页面出现纵向滚动条。务必在全局 CSS 中添加:
html, body { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; }
canvas { display: block; width: 100%; height: 100%; }
二、核心机制解析:为什么必须调用 updateProjectionMatrix
许多初学者容易产生疑问:既然已经修改了 camera.aspect = width / height,为什么还要额外显式调用一次 camera.updateProjectionMatrix()?
在 3D 渲染管线中,CPU 计算物体在屏幕上的二维投影位置依赖于一个 4×4 的数学矩阵,即投影矩阵(Projection Matrix)。修改 camera.aspect 只是修改了相机对象上的一个普通数值属性,并没有直接改变该 4×4 矩阵内部的 16 个具体浮点数值。
如果不调用 updateProjectionMatrix(),渲染管线将继续使用上一次旧的投影矩阵。此时,由于 renderer.setSize() 已经把 <canvas> 实际尺寸拉大或缩小,原本比例的渲染结果就会被强行缩放匹配到新尺寸的 Canvas 像素中,导致球体被压扁成椭球或拉长变形。
三、局部容器与现代前端组件化方案(ResizeObserver)
在 Vue、React 项目或数据可视化仪表盘中,Three.js 往往不是铺满全屏,而是嵌入在某个指定 <div> 容器内部。这种情况下,直接监听 window.resize 存在明显缺陷:
- 侧边栏折叠/展开:浏览器窗口自身尺寸并未改变,但容器
div宽度改变,window.resize无法被触发。 - Flex / Grid 弹性排版:其他同级 DOM 元素的收缩导致容器尺寸改变,窗口全局事件同样无法感知。
- 获取尺寸途径不同:不能再使用
window.innerWidth,而必须通过container.clientWidth和container.clientHeight获取真实尺寸。
2. ResizeObserver 规范实现代码
/**
* 局部容器响应式初始化函数
* @param {HTMLElement} container 挂载 Three.js 的 DOM 容器
*/
function initThreeInContainer(container) {
const scene = new THREE.Scene();
// 初始以容器当前宽高计算
let width = container.clientWidth;
let height = container.clientHeight;
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 观察容器本身的几何尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 获取容器最新的内容框尺寸
const newWidth = entry.contentRect.width;
const newHeight = entry.contentRect.height;
// 避免极端情况下宽高为 0(如 display: none)引发除零错误
if (newWidth === 0 || newHeight === 0) return;
camera.aspect = newWidth / newHeight;
camera.updateProjectionMatrix();
renderer.setSize(newWidth, newHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
});
resizeObserver.observe(container);
// 返回清理函数,便于在组件销毁(unmount)时释放资源,避免内存泄露
return function cleanup() {
resizeObserver.disconnect();
renderer.dispose();
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
};
}
四、设备像素比(setPixelRatio)与性能平衡
在 MacBook Retina 屏幕或高分辨率智能手机上,系统 window.devicePixelRatio(DPR)通常达到 2 甚至 3 以上。若直接执行 renderer.setPixelRatio(window.devicePixelRatio),在 3K/4K 屏幕上,Three.js 需要计算的像素点数量将剧增 4 至 9 倍,极易引发移动设备过热、降频并导致帧率骤降。
行业通用的最佳实践是将像素比限制在最高 2.0:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
限制为 2 既能保证在 Retina 视网膜屏幕上获得极高锐度、消除锯齿,又能将片元着色器的计算负荷控制在合理范围内,避免不必要的性能浪费。
五、正交相机(OrthographicCamera)的自适应差异
如果项目使用的是正交相机(常见于 2D 视角、CAD 制图、工程俯视图或等距像素风游戏),其视锥体不是角度展开,而是一个长方体。此时相机没有 aspect 属性,而是通过 left、right、top、bottom 四个边界平面界定视野。
正交相机的响应式调整通常采取以下方式更新视界范围并刷新矩阵:
function onOrthographicResize() {
const width = window.innerWidth;
const height = window.innerHeight;
const frustumSize = 10; // 固定的垂直视野单位跨度
const aspect = width / height;
camera.left = (-frustumSize * aspect) / 2;
camera.right = (frustumSize * aspect) / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
六、自查清单与常见陷阱排查
在开发与验收阶段,可以按照下方的响应式关键要素清单逐项验证:
| 常见问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 页面出现 3~5px 额外滚动条 | <canvas> 默认为 inline 元素,基线留白 |
CSS 增加 canvas { display: block; } |
| 窗口拉宽时球体被压成椭圆 | 未执行 camera.updateProjectionMatrix() |
在修改 camera.aspect 后立即调用该方法刷新投影矩阵 |
| 侧边栏收起时画布未自适应 | 仅监听了 window.resize,窗口大小未变 |
使用 ResizeObserver 监听挂载容器的元素尺寸变化 |
| 高清屏上画面模糊或发虚 | 未同步设备像素比,使用默认 1.0 渲染 | 调用 renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) |
| 非循环渲染场景下画面不随窗口刷新 | 按需渲染模式下未在 resize 回调中触发绘制 | 在 onResize 回调的最后一行显式执行 renderer.render(scene, camera) |
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52
JavaScript能做什么?前端开发中的作用与应用场景解析
2026-11-04 23:17:54
相关标签:








