three.js CSS3DObject 详解:用 CSS3DRenderer 将 DOM 元素纳入 3D 场景图
CSS3DObject 是 three.js 中用于把任意 DOM 元素包装成 3D 场景节点的基础对象,它由 CSS3DRenderer 渲染,通过 CSS3 transform 属性实现对 DOM 元素的层级化 3D 变换。读完本文,你将掌握 CSS3DObject 的完整 API(构造函数、属性与底层行为),理解渲染器如何把 matrixWorld 转译为 CSS 矩阵,并能参考仓库官方示例(元素周期表、WebGL 混合渲染)独立完成“DOM 即 3D 对象”的实战开发。
一、CSS3DObject 的定位与继承关系
官方文档页面 CSS3DObject 给出其继承链:
Inheritance: EventDispatcher → Object3D → CSS3DObject
也就是说,CSS3DObject 直接继承自 three.js 核心类 Object3D,具备完整的场景图能力:position、rotation、scale、add()、traverse()、layers、onBeforeRender 等 API 全部可用。它本身不携带任何几何体或材质,其“外观”完全由传入的 DOM 元素决定。
与之配套的是同文件导出的 CSS3DRenderer 与 CSS3DSprite,三者共同定义在 examples/jsm/renderers/CSS3DRenderer.js:
CSS3DRenderer:负责把场景中所有CSS3DObject/CSS3DSprite的位置、旋转、缩放转译为 DOM 元素的 CSStransform;CSS3DSprite:CSS3DObject的 billboard(广告牌)变体,元素始终面向相机;CSS3DObject:本文主角,承载“DOM 元素 + 场景图变换”这一基础语义。
CSS3DRenderer 的文档注释明确说明了适用场景:既可用于在不使用 canvas 渲染的情况下给网站施加 3D 效果,也可用于将 DOM 元素与 WebGL 内容组合(见 CSS3DRenderer 文档页)。
二、引入方式:作为 Addon 显式导入
CSS3DObject 属于 addon(扩展模块),不在 three 核心包中,必须显式从 addons 路径导入:
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
仓库 package.json 的 exports 字段保证了该导入路径的有效性:"./addons/*": "./examples/jsm/*",因此 three/addons/renderers/CSS3DRenderer.js 实际解析到仓库中的 examples/jsm/renderers/CSS3DRenderer.js。
在使用原生 import map 的 HTML 页面(不经过打包器)中,可参照官方示例的写法(见 examples/css3d_periodictable.html):
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
三、构造函数:new CSS3DObject( element )
3.1 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
element |
HTMLElement |
document.createElement('div') |
定义该 3D 对象外观的 DOM 元素,可以是 div、iframe、img 等任意 HTML 元素 |
源码位于 CSS3DRenderer.js 第 20-84 行,构造函数签名为 constructor( element = document.createElement( 'div' ) )。
3.2 构造函数内部自动完成的四件事
阅读构造函数实现(第 27-72 行),创建 CSS3DObject 时框架会自动:
- 统一内联样式:
element.style.position = 'absolute'、pointerEvents = 'auto'、userSelect = 'none',并设置draggable属性为false。这让 DOM 元素脱离正常文档流,交给渲染器的transform控制,同时默认保留自身交互能力(pointerEvents: auto表示元素上的链接、按钮可点击); - 类型标记:
this.isCSS3DObject = true; - 保存元素引用:
this.element = element(只读); - 注册
removed监听:当对象从场景图中被移除时,traverse其所有子对象,把仍挂载在文档中的element从 DOM 中remove()。这意味着从场景中删除CSS3DObject会同步清理 DOM,避免残留“幽灵”元素。
constructor( element = document.createElement( 'div' ) ) {
super();
this.isCSS3DObject = true;
this.element = element;
this.element.style.position = 'absolute';
this.element.style.pointerEvents = 'auto';
this.element.style.userSelect = 'none';
this.element.setAttribute( 'draggable', false );
this.addEventListener( 'removed', function () {
this.traverse( function ( object ) {
if (
object.element &&
object.element instanceof object.element.ownerDocument.defaultView.Element &&
object.element.parentNode !== null
) {
object.element.remove();
}
} );
} );
}
3.3 copy():元素会随对象一起深拷贝
CSS3DObject 覆写了 Object3D.copy(),在复制变换等场景图属性的同时,会用 source.element.cloneNode( true ) 把 DOM 元素连同其子树完整克隆到新对象上(第 74-82 行):
copy( source, recursive ) {
super.copy( source, recursive );
this.element = source.element.cloneNode( true );
return this;
}
因此克隆一个 CSS3DObject 不会与原对象共享同一个 DOM 节点,这是把它当作可复制的场景图节点使用时的关键保障。
四、属性一览
4.1 .element : HTMLElement(只读)
定义该 3D 对象外观的 DOM 元素。渲染器每帧读取的场景对象只有 element 与 matrixWorld,所以对 element 的常规 DOM 操作(修改内容、绑定事件、切换 class)都不会影响 3D 变换。
4.2 .isCSS3DObject : boolean(只读)
类型测试标记,默认值为 true。典型用途是在遍历场景图时区分节点类型:
scene.traverse( ( object ) => {
if ( object.isCSS3DObject ) {
// 该节点是一个 DOM 包装对象
}
} );
渲染器内部正是依靠这个标记(以及 isCSS3DSprite)决定对子树的处理分支(renderObject,第 362-440 行)。
五、渲染管线:matrixWorld 如何变成 CSS transform
CSS3DObject 自身不含渲染逻辑,真正“画”出它的是 CSS3DRenderer.render( scene, camera )。理解这条调用链,是正确摆放 CSS3DObject 的前提:
- 相机层:
render先用camera.projectionMatrix.elements[ 5 ] * _heightHalf计算视口焦距fov,再把camera.matrixWorldInverse拼成matrix3d(...),配合perspective(fov px)(透视相机)或正交相机的scale + translate组合,写入cameraElement.style.transform。相机样式带缓存,只有变换变化时才写 DOM; - 对象层:
renderObject递归遍历场景图。对每个可见且通过object.layers.test( camera.layers )的CSS3DObject,取其object.matrixWorld生成getObjectCSSMatrix—— 即translate(-50%,-50%) matrix3d(...16 个分量...)。开头的translate(-50%,-50%)使元素以自身中心为原点参与 3D 变换,与Object3D的“位置即中心”语义一致; - 缓存与挂载:对象的样式字符串存入
WeakMap(cache.objects),只有transform真正变化时才更新;元素始终被挂到cameraElement容器下,visible === false的对象则整棵子树设为display: none; - 渲染回调:写入 transform 前后会分别调用
object.onBeforeRender( renderer, scene, camera )与object.onAfterRender(...),与 three.js 核心渲染器的事件约定保持一致,便于在其中做逐帧调整。
从源码结构看,正交相机与透视相机走不同的 CSS 前缀:正交相机使用 scale(fov) translate(tx, ty) matrix3d(...),透视相机使用 perspective(fov) translateZ(fov) matrix3d(...)。因此 CSS3DObject 同时兼容 PerspectiveCamera 和 OrthographicCamera(官方提供了 examples/css3d_orthographic.html 正交相机示例验证)。
渲染器的关键 API 速查
| API | 说明 |
|---|---|
new CSS3DRenderer( parameters ) |
parameters.element 可选,指定渲染器追加子元素的容器;不传则新建 div |
.domElement |
渲染器容器元素,需手动 appendChild 到页面并设置 setSize |
.render( scene, camera ) |
每帧调用,遍历场景更新所有 CSS3DObject 的 transform |
.setSize( width, height ) |
设置容器与视口尺寸,窗口 resize 时必须同步调用 |
.getSize() |
返回 { width, height } |
六、实战示例一:CSS3DObject 驱动的元素周期表
仓库示例 examples/css3d_periodictable.html 是 CSS3DObject 最典型的用法:用纯 DOM 卡片构建元素周期表,在 TABLE / SPHERE / HELIX / GRID 四种空间布局间做补间动画,完全不用 WebGL。
核心步骤拆解(对应示例中 init() 与渲染循环):
- 构建 DOM 卡片:每个元素是一个
div.element,内含原子序数、符号、名称与相对原子质量子节点; - 包装并加入场景:
const element = document.createElement( 'div' );
element.className = 'element';
// ... 追加 number / symbol / details 子节点
const objectCSS = new CSS3DObject( element );
objectCSS.position.x = Math.random() * 4000 - 2000;
objectCSS.position.y = Math.random() * 4000 - 2000;
objectCSS.position.z = Math.random() * 4000 - 2000;
scene.add( objectCSS );
注意这里只设置 position,CSS3DObject 继承自 Object3D,旋转、缩放、嵌套进 Group 等一切场景图操作都可用;
- 创建渲染器并挂载:
renderer = new CSS3DRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.getElementById( 'container' ).appendChild( renderer.domElement );
- 交互:
TrackballControls直接绑定在renderer.domElement上,因为 CSS3D 渲染走 DOM 而非 canvas,控制器必须使用 CSS3D 渲染器的容器才能接收事件:
controls = new TrackballControls( camera, renderer.domElement );
controls.minDistance = 500;
controls.maxDistance = 6000;
controls.addEventListener( 'change', render );
- 每帧渲染:
function render() {
renderer.render( scene, camera );
}
七、实战示例二:WebGL 与 CSS3DObject 混合渲染
examples/css3d_mixed.html 展示了文档注释中提到的另一大应用场景——DOM 与 WebGL 内容组合。它把 CSS3DObject 包装的 iframe 放进与 WebGL 场景共用同一 Scene 和 Camera 的“相框”中,两者逐帧同步变换:
rendererCSS3D = new CSS3DRenderer();
rendererCSS3D.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( rendererCSS3D.domElement );
rendererWebGL = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
rendererWebGL.domElement.style.position = 'absolute';
rendererWebGL.domElement.style.top = '0';
rendererWebGL.domElement.style.pointerEvents = 'none'; // 让事件穿透给下层
// ...
const scene = new THREE.Scene();
// scene 中同时放入 WebGL 的 Mesh / LineSegments 与 CSS3DObject
// 把 iframe 包成 3D 对象,加进同一个 scene
const iframe = document.createElement( 'iframe' );
iframe.style.width = '1028px';
iframe.style.height = '768px';
iframe.style.border = '0px';
iframe.style.backfaceVisibility = 'hidden';
iframe.src = './#webgl_animation_keyframes';
scene.add( new CSS3DObject( iframe ) );
function animate() {
controls.update();
rendererWebGL.render( scene, camera );
rendererCSS3D.render( scene, camera ); // 同一个 scene、同一个 camera
}
该示例中有三个值得注意的混合渲染技巧:
- 事件穿透与切换:WebGL canvas 设置
pointerEvents = 'none',交互交给独立的全屏controlsDomElement;同时在OrbitControls的start/end事件里临时把iframe.style.pointerEvents切换为none/auto,保证拖动相机时 iframe 不“截获”鼠标; backfaceVisibility: 'hidden':避免 iframe 翻转 180 度后显示为镜像;- alpha 混合:WebGL 渲染器开启
alpha: true,让 CSS3D 层与 WebGL 层在页面中正确叠放。
仓库中还有更多同类示例可供参考:examples/css3d_molecules.html(分子结构)、examples/css3d_youtube.html(YouTube 视频作为 3D 面)、examples/css3d_sandbox.html(交互沙盒)、examples/css3d_orthographic.html(正交相机)。
八、进阶:CSS3DSprite 与使用限制
8.1 同类对象 CSS3DSprite
CSS3DSprite 继承自 CSS3DObject(第 93-133 行),把 DOM 元素渲染为始终面向相机的 sprite:渲染器在 renderObject 中检测到 object.isCSS3DSprite 后,用相机姿态矩阵替换对象自身的旋转(保留位置与缩放),并额外支持 rotation2D 属性(弧度)在屏幕平面内旋转元素。copy() 也会同步复制 rotation2D。示例见 examples/css3d_sprites.html。
8.2 使用限制(务必知悉)
CSS3DRenderer 的 JSDoc 注释(第 140-156 行)列出了三条硬限制:
- 不能使用 three.js 的材质系统——外观完全由 DOM/CSS 决定,
MeshStandardMaterial等与之无关; - 不能使用几何体——没有
BufferGeometry、光照、阴影参与; - 仅支持 100% 的浏览器/显示器缩放——CSS 像素与 three.js 世界单位之间的换算依赖 1:1 的屏幕缩放,浏览器缩放比例非 100% 时会出现透视错位。
另外,由于 CSS3DObject 的交互能力来自真实 DOM,它也天然具备 WebGL canvas 难以做到的优势:文本可选中排版(除非 userSelect 被改写)、链接可跳转、可内嵌 iframe 运行任意 Web 应用。
九、小结
CSS3DObject继承Object3D,以element(DOM 元素)+ 场景图变换为核心,由CSS3DRenderer通过 CSSmatrix3d转译渲染;- 构造时自动接管元素定位样式并注册 DOM 清理逻辑,
copy()通过cloneNode(true)深拷贝元素; - 与
CSS3DSprite组合可覆盖“平面 3D 对象”与“广告牌”两类需求; - 典型场景:无 canvas 的 DOM 3D 站点(元素周期表)、WebGL 与 DOM 混合渲染(iframe 相框)、可交互 3D 界面;
- 全部源码集中于 examples/jsm/renderers/CSS3DRenderer.js,文档页为 docs/pages/CSS3DObject.html,配合
examples/css3d_*.html系列示例即可完整复现本文所有用法。
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 StartedRust0624
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
