three.js CSS3DRenderer 深度解析:用 CSS3 Transform 把 DOM 元素放进 3D 场景
CSS3DRenderer 是 three.js 的附加渲染器,它通过 CSS3 transform 属性对普通 DOM 元素施加层级化的 3D 变换,让你无需 Canvas 渲染即可在网页中呈现 3D 效果,也可以把 DOM 元素与 WebGL 内容混合渲染。读完本文,你将掌握 CSS3DRenderer 的完整 API(构造参数、domElement、getSize/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 对象——CSS3DObject 或 CSS3DSprite——再添加到场景图中。
文档同时明确列出三条重要限制,选型前必须知晓:
- 无法使用 three.js 的材质系统(material system);
- 无法使用几何体(geometries);
- 渲染器只支持浏览器和显示器的 100% 缩放(页面被缩放后 CSS 矩阵与视口的换算会失准)。
换句话说,CSS3DRenderer 只聚焦于普通 DOM 元素本身,几何与材质请交给 WebGLRenderer 或 WebGPURenderer。
二、导入方式与最小可运行示例
CSS3DRenderer 是一个 addon,必须显式导入(对应手册 Installation 章节的 Addons 说明):
import { CSS3DRenderer } from 'three/addons/renderers/CSS3DRenderer.js';
同一模块还导出了配套的 CSS3DObject 与 CSS3DSprite(见 examples/jsm/renderers/CSS3DRenderer.js#L454 的 export 语句):
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 可见,渲染时的视距fov由camera.projectionMatrix.elements[5] * _heightHalf计算,_heightHalf来自setSize,未设置时结果不正确;- 渲染器创建的
domElement会被写入overflow: hidden(L178),所有 3D 元素最终挂在内部的cameraElement下,父容器只需提供一个定位区域; - 官方示例 css3d_periodictable.html#L351-L353 正是"创建渲染器 →
setSize→appendChild(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 透视:
domElement(overflow: hidden)——对外暴露的挂载点,即.domElement属性指向的元素;viewElement(L187-L190)——transformOrigin: '0 0'、pointerEvents: 'none',用于承载相机视口偏移(view offset)的平移与缩放;cameraElement(L192-L196)——transformStyle: 'preserve-3d',这是整棵 DOM 树获得 3D 空间合成关系的关键,所有CSS3DObject的元素最终被追加到该节点下(L422-L426)。
.domElement : HTMLElement
渲染器追加子元素的 DOM 容器。注意它默认没有被自动插入页面,需要手动 appendChild 到文档中。
.getSize() : Object
返回包含 width 和 height 的对象(L203-L210)。
.render( scene, camera )
用给定相机渲染给定场景,scene 可以是 Scene 或任意其他类型的 3D 对象。这是每帧(或每次交互/动画更新后)必须调用的方法。
.setSize( width, height )
把渲染器调整为指定宽高,同时同步设置 domElement、viewElement、cameraElement 三层元素的 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(...),其中matrix3d由camera.matrixWorldInverse转换而来(L249-L252); - 正交相机(
camera.isOrthographicCamera):改为scale(fov)加左右/上下中点平移translate(tx, ty)(L241-L250),对应仓库中的 css3d_orthographic.html 示例; - 相机视口偏移:若启用了
camera.view(渲染目标子区域),还会在viewElement上叠加相应的translate与scale(L222-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 的关键细节
CSS3DObject(L20-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/isCSS3DSprite;CSS3DSprite 额外提供 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 对 position 与 rotation 做指数缓动插值,同时用 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-L61、L91-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));
混合排布的核心技巧可以归纳为四点:
- 同一 camera、同一 scene 驱动两个渲染器,保证视空间严格对齐;
- WebGL 渲染器开启
alpha: true,其 canvas 叠在 CSS3D 层之上,并设pointer-events: none,让鼠标事件穿透到下层 DOM(示例中甚至会在拖动相机时临时禁用 iframe 的事件,L107-L108); - 在 DOM 元素背后放一个
NoBlending + opacity: 0的 mesh 制造 WebGL 层"镂空",让 iframe 内容不被 WebGL 背景盖住; - iframe 设置
backface-visibility: hidden,避免背面镜像。
六、实战注意事项
结合 API 文档限制与源码实现,使用 CSS3DRenderer 时建议遵守:
- 保持 100% 页面缩放:文档明确该渲染器只支持 100% 浏览器/显示器缩放,这是其 CSS 像素矩阵假设的前提;
- resize 三件套:
camera.aspect更新 +camera.updateProjectionMatrix()+renderer.setSize(),三者缺一不可(参考 css3d_periodictable.html#L426-L435); - 元素尺寸与相机距离匹配:DOM 元素以 CSS 像素计量,建议像 periodictable 示例那样把
camera.position.z、near/far 都按元素尺寸放大,否则元素会显得过小或近裁剪; - 交互控制:
CSS3DObject默认pointer-events: auto,可交互的 DOM(按钮、iframe)在叠加轨道控制器时要处理好事件冲突,css3d_mixed.html的start/end回调切换pointerEvents是现成方案; - 可见性控制:可利用
visible与 three.js 的layers机制(渲染器内部会做layers.test(camera.layers)过滤,L374)按图层批量显隐; - 广告牌需求用 CSS3DSprite:需要"永远面向相机"的标签用
CSS3DSprite,配合rotation2D实现屏幕内旋转,无需手动lookAt计算; - 清理:从场景移除
CSS3DObject时其 DOM 会自动清理(removed事件监听,L54-L70),无需手动element.remove()。
参考文件
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00