首页
/ three.js CSS3DRenderer 深度解析:用 CSS3 Transform 把 DOM 元素放进 3D 场景

three.js CSS3DRenderer 深度解析:用 CSS3 Transform 把 DOM 元素放进 3D 场景

2026-09-06 13:12:27作者:苗圣禹Peter

CSS3DRenderer 是 three.js 的附加渲染器,它通过 CSS3 transform 属性对普通 DOM 元素施加层级化的 3D 变换,让你无需 Canvas 渲染即可在网页中呈现 3D 效果,也可以把 DOM 元素与 WebGL 内容混合渲染。读完本文,你将掌握 CSS3DRenderer 的完整 API(构造参数、domElementgetSize/render/setSize)、CSS3DObject/CSS3DSprite 的包装方式,并能基于仓库中的真实示例(元素周期表 3D 布局、DOM 与 WebGL 混合渲染)复制出可运行的实战方案。

一、核心定位与能力边界

CSS3DRenderer 的定位非常明确(见 API 文档 与源码中的类注释 examples/jsm/renderers/CSS3DRenderer.js#L140-L156):

  • 无 Canvas 渲染的 3D 网站效果:如果你只想对网页元素施加 3D 变换(旋转、景深透视、层叠),又不想引入 WebGL 渲染管线,CSS3DRenderer 就是为此设计的;
  • DOM 与 WebGL 内容混排:它也可以与 WebGLRenderer 同时工作,把可交互的 HTML(如 iframe、按钮、文字)与 3D 几何体组合在同一视角下;
  • 基于 three 场景图:DOM 元素本身不是 3D 对象,需要先包装成特殊的 3D 对象——CSS3DObjectCSS3DSprite——再添加到场景图中。

文档同时明确列出三条重要限制,选型前必须知晓:

  1. 无法使用 three.js 的材质系统(material system);
  2. 无法使用几何体(geometries);
  3. 渲染器只支持浏览器和显示器的 100% 缩放(页面被缩放后 CSS 矩阵与视口的换算会失准)。

换句话说,CSS3DRenderer 只聚焦于普通 DOM 元素本身,几何与材质请交给 WebGLRendererWebGPURenderer

二、导入方式与最小可运行示例

CSS3DRenderer 是一个 addon,必须显式导入(对应手册 Installation 章节的 Addons 说明):

import { CSS3DRenderer } from 'three/addons/renderers/CSS3DRenderer.js';

同一模块还导出了配套的 CSS3DObjectCSS3DSprite(见 examples/jsm/renderers/CSS3DRenderer.js#L454export 语句):

import { CSS3DRenderer, CSS3DObject, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';

仓库示例中通过 importmap 把 three/addons/ 映射到 ./jsm/ 目录(参见 examples/css3d_periodictable.html#L98-L105):

<script type="importmap">
  {
    "imports": {
      "three": "../build/three.module.js",
      "three/addons/": "./jsm/"
    }
  }
</script>

下面是一个基于官方示例 examples/css3d_periodictable.html 抽象出的最小流程:

import * as THREE from 'three';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// 1. 场景与相机(与 WebGL 流程一致)
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 3000;
const scene = new THREE.Scene();

// 2. 创建 DOM 元素并包装为 CSS3DObject
const element = document.createElement('div');
element.style.width = '120px';
element.style.height = '160px';
element.textContent = 'three.js CSS3D';
const objectCSS = new CSS3DObject(element);
scene.add(objectCSS);

// 3. 创建渲染器并挂载
const renderer = new CSS3DRenderer();        // 不传参数时内部自动创建 div
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

// 4. 渲染与自适应
function render() {
  renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
});
render();

几个要点:

  • setSize() 必须在首次 render() 前调用。从源码 examples/jsm/renderers/CSS3DRenderer.js#L220 可见,渲染时的视距 fovcamera.projectionMatrix.elements[5] * _heightHalf 计算,_heightHalf 来自 setSize,未设置时结果不正确;
  • 渲染器创建的 domElement 会被写入 overflow: hiddenL178),所有 3D 元素最终挂在内部的 cameraElement 下,父容器只需提供一个定位区域;
  • 官方示例 css3d_periodictable.html#L351-L353 正是"创建渲染器 → setSizeappendChild(renderer.domElement)"的完整范式。

三、构造函数与 Parameters 类型

new CSS3DRenderer( parameters )

对应文档中的 new CSS3DRenderer( parameters : CSS3DRenderer~Parameters ),源码实现在 examples/jsm/renderers/CSS3DRenderer.js#L164-L196

参数 类型 说明
element HTMLElement(可选) 渲染器向其追加子元素的容器。不传时会新建一个 div 作为 domElement

源码中 domElement 的确定逻辑为(L176):

const domElement = parameters.element !== undefined
  ? parameters.element
  : document.createElement('div');

从源码结构看,渲染器内部维护三层嵌套 DOM,这一结构解释了它为何能产生真实 3D 透视:

  1. domElementoverflow: hidden)——对外暴露的挂载点,即 .domElement 属性指向的元素;
  2. viewElementL187-L190)——transformOrigin: '0 0'pointerEvents: 'none',用于承载相机视口偏移(view offset)的平移与缩放;
  3. cameraElementL192-L196)——transformStyle: 'preserve-3d',这是整棵 DOM 树获得 3D 空间合成关系的关键,所有 CSS3DObject 的元素最终被追加到该节点下(L422-L426)。

.domElement : HTMLElement

渲染器追加子元素的 DOM 容器。注意它默认没有被自动插入页面,需要手动 appendChild 到文档中。

.getSize() : Object

返回包含 widthheight 的对象(L203-L210)。

.render( scene, camera )

用给定相机渲染给定场景,scene 可以是 Scene 或任意其他类型的 3D 对象。这是每帧(或每次交互/动画更新后)必须调用的方法。

.setSize( width, height )

把渲染器调整为指定宽高,同时同步设置 domElementviewElementcameraElement 三层元素的 CSS 宽高(L275-L291)。窗口 resize 时应与相机的 aspect 更新、updateProjectionMatrix() 一起调用。

四、源码级原理:一次 render() 里发生了什么

.render(scene, camera) 的完整实现在 examples/jsm/renderers/CSS3DRenderer.js#L218-L267,可以拆解为四个阶段:

1. 计算视距与相机矩阵

const fov = camera.projectionMatrix.elements[5] * _heightHalf;

projectionMatrix 第 5 个元素与视口半高的乘积,等价于相机焦距对应的 CSS perspective() 像素值。随后:

  • 透视相机:生成 perspective(fov px) translateZ(fov px) matrix3d(...),其中 matrix3dcamera.matrixWorldInverse 转换而来(L249-L252);
  • 正交相机camera.isOrthographicCamera):改为 scale(fov) 加左右/上下中点平移 translate(tx, ty)L241-L250),对应仓库中的 css3d_orthographic.html 示例;
  • 相机视口偏移:若启用了 camera.view(渲染目标子区域),还会在 viewElement 上叠加相应的 translatescaleL222-L234);
  • 最后统一 translate(widthHalf, heightHalf) 把原点移到屏幕中心。

坐标系的细节值得注意:getCameraCSSMatrix()L299-L322)在写出 matrix3d 时对第 2、6、10、14 个分量(即 Y 通道)取负——CSS 的 Y 轴向下,而 three.js 的 Y 轴向上,必须翻转后才能与 WebGL 相机对齐。同时 epsilon() 辅助函数把绝对值小于 1e-10 的值归零,避免浮点尾数污染 CSS 矩阵字符串。

2. 样式缓存,避免无谓重排

相机样式写入前会先与 cache.camera.style 比较(L257-L263);每个对象也有独立的 WeakMap 缓存(L411-L420),只有变换矩阵字符串真正变化时才更新 element.style.transform。这意味着静止场景的重复 render() 几乎不产生 DOM 写入开销。

3. 递归遍历场景图

renderObject()L362-L440)递归处理每个节点:

  • visible === false 的整棵子树被 hideObject 设为 display: none
  • CSS3DObject,还会执行 图层过滤object.layers.test(camera.layers) 为 false 时同样隐藏(L374-L377)——这是它相对 CSS2D 渲染器的一个能力点,可以用 three.js 的 layers 机制做对象级可见性控制;
  • 可见对象在更新 transform 前后分别回调 object.onBeforeRender() / object.onAfterRender(),可在此挂接自定义逻辑;
  • 元素若尚未挂载到 cameraElement 会被追加进去,保证 DOM 层级与场景树一致。

4. 普通对象 vs Sprite 的矩阵差异

  • CSS3DObject:直接使用 getObjectCSSMatrix(object.matrixWorld),输出 translate(-50%,-50%) + matrix3d(...)L324-L348)——前置的 translate(-50%,-50%) 让 DOM 元素的中心而非左上角对齐 3D 坐标点;
  • CSS3DSprite( billboard 广告牌):按注释引用的 billboard 矩阵构造法(L385-L404),以相机世界逆矩阵的转置作为朝向基,若设置了 rotation2D(弧度)再左乘一个 makeRotationZ(rotation2D) 的二维旋转,最后只保留对象的位置与缩放分量。效果是元素始终面向相机,且可用 rotation2D 在屏幕平面内自转,对应示例 css3d_sprites.html

CSS3DObject / CSS3DSprite 的关键细节

CSS3DObjectL20-L84)继承自 Object3D,构造时对传入元素做了如下统一处理:

this.element.style.position = 'absolute';   // 必须由 3D transform 定位
this.element.style.pointerEvents = 'auto';  // 保留交互(按钮、链接可点击)
this.element.style.userSelect = 'none';     // 拖拽视角时防止选中文字
this.element.setAttribute('draggable', false);

它还监听自身的 removed 事件:当对象从场景中移除时,自动 remove() 其 DOM 节点(L54-L70),避免残留节点。copy() 则通过 element.cloneNode(true) 深拷贝 DOM(L74-L82)。类型判定标志为 isCSS3DObject/isCSS3DSpriteCSS3DSprite 额外提供 rotation2D(弧度,默认 0,见 CSS3DSprite 文档)。

五、官方示例实战

仓库在 examples/ 下提供了完整的 CSS3D 示例家族,可作为不同场景的参考实现:

示例 演示内容
css3d_periodictable.html 118 个元素 DOM 卡片在 表格/球面/螺旋/网格 四种布局间做补间变换
css3d_mixed.html CSS3D 与 WebGL 双渲染器混合,DOM 中嵌入 iframe
css3d_orthographic.html 正交相机下的 CSS3D 渲染
css3d_sandbox.html 相机沙盒调试
css3d_sprites.html CSS3DSprite 广告牌与 rotation2D
css3d_youtube.html / css3d_molecules.html 3D 空间中嵌入视频 / 分子结构展示

案例 1:大规模 DOM 元素编排(periodictable)

css3d_periodictable.html 展示了 CSS3DRenderer 的典型用法:为每个化学元素创建一个带样式的 div(元素符号、原子量等),包装成 CSS3DObject 后随机撒布在 4000³ 的空间中(L254-L291),再预计算四套目标位姿(table/sphere/helix/grid),用 TWEEN 对 positionrotation 做指数缓动插值,同时用 TrackballControls(camera, renderer.domElement) 提供轨道交互(L357-L360)。注意其相机焦距取 fov = 40、近远平面 1, 10000——CSS3D 场景中 DOM 元素以像素为单位布局,near/far 和相机距离通常要比常规 WebGL 场景放大一到两个数量级。

案例 2:DOM 与 WebGL 混合渲染(mixed)

css3d_mixed.html 演示了文档所说的"combine DOM elements with WebGL content"。关键做法(L35-L61L91-L102):

rendererCSS3D = new CSS3DRenderer();
rendererCSS3D.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(rendererCSS3D.domElement);

rendererWebGL = new THREE.WebGLRenderer({ antialias: true, alpha: true }); // alpha 透明
rendererWebGL.domElement.style.position = 'absolute';
rendererWebGL.domElement.style.pointerEvents = 'none';  // WebGL 层不拦截鼠标
// ...
const mesh = new THREE.Mesh(
  new THREE.PlaneGeometry(1024, 768),
  new THREE.MeshBasicMaterial({
    color: 0xff0000,
    blending: THREE.NoBlending,
    opacity: 0,
    premultipliedAlpha: true
  })
);
scene.add(mesh); // "抠洞":用不透明混合的空洞 mesh 遮挡 WebGL 层

const iframe = document.createElement('iframe');
iframe.style.backfaceVisibility = 'hidden';
iframe.src = './#webgl_animation_keyframes';
scene.add(new CSS3DObject(iframe));

混合排布的核心技巧可以归纳为四点:

  1. 同一 camera、同一 scene 驱动两个渲染器,保证视空间严格对齐;
  2. WebGL 渲染器开启 alpha: true,其 canvas 叠在 CSS3D 层之上,并设 pointer-events: none,让鼠标事件穿透到下层 DOM(示例中甚至会在拖动相机时临时禁用 iframe 的事件,L107-L108);
  3. 在 DOM 元素背后放一个 NoBlending + opacity: 0 的 mesh 制造 WebGL 层"镂空",让 iframe 内容不被 WebGL 背景盖住;
  4. iframe 设置 backface-visibility: hidden,避免背面镜像。

六、实战注意事项

结合 API 文档限制与源码实现,使用 CSS3DRenderer 时建议遵守:

  1. 保持 100% 页面缩放:文档明确该渲染器只支持 100% 浏览器/显示器缩放,这是其 CSS 像素矩阵假设的前提;
  2. resize 三件套camera.aspect 更新 + camera.updateProjectionMatrix() + renderer.setSize(),三者缺一不可(参考 css3d_periodictable.html#L426-L435);
  3. 元素尺寸与相机距离匹配:DOM 元素以 CSS 像素计量,建议像 periodictable 示例那样把 camera.position.z、near/far 都按元素尺寸放大,否则元素会显得过小或近裁剪;
  4. 交互控制CSS3DObject 默认 pointer-events: auto,可交互的 DOM(按钮、iframe)在叠加轨道控制器时要处理好事件冲突,css3d_mixed.htmlstart/end 回调切换 pointerEvents 是现成方案;
  5. 可见性控制:可利用 visible 与 three.js 的 layers 机制(渲染器内部会做 layers.test(camera.layers) 过滤,L374)按图层批量显隐;
  6. 广告牌需求用 CSS3DSprite:需要"永远面向相机"的标签用 CSS3DSprite,配合 rotation2D 实现屏幕内旋转,无需手动 lookAt 计算;
  7. 清理:从场景移除 CSS3DObject 时其 DOM 会自动清理(removed 事件监听,L54-L70),无需手动 element.remove()

参考文件

登录后查看全文
热门项目推荐
相关项目推荐