Three.js 怎么实现响应式布局以适应窗口大小变化

作者:页面旅途时间:2026-11-03 19:23:35浏览:422

详细讲解在 Three.js 中如何正确监听窗口和容器尺寸变化,同步更新透视相机宽高比与投影矩阵,调整渲染器输出尺寸与设备像素比,彻底解决 3D 场景拉伸变形与黑边问题。

技术专题:Three.js / WebGL 适配目标:浏览器全屏与局部容器 核心要点:相机投影矩阵重算与渲染器尺寸重设
Three.js 怎么实现响应式布局以适应窗口大小变化
图 0:Three.js 响应式适配核心链路概览

在默认情况下,Three.js 创建的 <canvas> 画布并不会随浏览器窗口变化而自动伸缩。当用户缩放窗口、旋转移动端屏幕或折叠侧边栏时,画面往往会出现两种异常:一是 3D 物体被硬生生横向或纵向拉伸变形,二是画布周围出现空白黑边或滚动条。实现 Three.js 响应式布局的核心,是在容器尺寸发生变化时,同步更新相机的视锥体宽高比(aspect)与投影矩阵,并重新设定渲染器的像素尺寸与设备像素比。本文将针对全屏与局部容器两种场景,提供可直接落地的完整代码与原理剖析。

一、全屏场景下的标准实现方案

全屏应用是最常见的 Three.js 展示形态。在此类场景中,画布占满整个浏览器视口。让全屏画面自动适应窗口变化,需要依次完成三个动作:绑定 resize 事件、更新透视相机参数,以及同步修改渲染器的像素尺寸。

Three.js 怎么实现响应式布局以适应窗口大小变化
图 1:Three.js 响应式适配 4 步标准化操作顺序

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();
CSS 关键约束:默认情况下 <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 像素中,导致球体被压扁成椭球或拉长变形。

Three.js 怎么实现响应式布局以适应窗口大小变化
图 2:是否重构投影矩阵对最终画面几何形变的影响对比

三、局部容器与现代前端组件化方案(ResizeObserver)

在 Vue、React 项目或数据可视化仪表盘中,Three.js 往往不是铺满全屏,而是嵌入在某个指定 <div> 容器内部。这种情况下,直接监听 window.resize 存在明显缺陷:

  • 侧边栏折叠/展开:浏览器窗口自身尺寸并未改变,但容器 div 宽度改变,window.resize 无法被触发。
  • Flex / Grid 弹性排版:其他同级 DOM 元素的收缩导致容器尺寸改变,窗口全局事件同样无法感知。
  • 获取尺寸途径不同:不能再使用 window.innerWidth,而必须通过 container.clientWidth 和 container.clientHeight 获取真实尺寸。
Three.js 怎么实现响应式布局以适应窗口大小变化
图 3:全屏视口方案与局部容器 ResizeObserver 方案选型与特性对比

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);
}

六、自查清单与常见陷阱排查

在开发与验收阶段,可以按照下方的响应式关键要素清单逐项验证:

Three.js 怎么实现响应式布局以适应窗口大小变化
图 4:Three.js 响应式布局自查清单与实施要点汇总
常见问题现象 根本原因 解决方案
页面出现 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)
作者最新文章

相关标签:

相关文章
更多