Three.js CSS2DObject 详解:把 DOM 元素包装成 3D 对象的实现与实战
CSS2DObject 是 three.js 中专门配合 CSS2DRenderer 使用的 3D 对象类型:它把一个普通 DOM 元素(HTMLElement)包装进场景图,让 HTML 标签、卡片、图标可以跟随 3D 场景中的坐标进行透视投影定位。本文基于官方文档 CSS2DObject 与 addon 源码 CSS2DRenderer.js,完整讲解其继承关系、构造参数、center / element / isCSS2DObject 属性,以及它在渲染管线中如何完成投影、可见性判断、z 序排序,并给出可运行的完整示例代码。
定位与继承体系:场景图中唯一的“HTML 节点”
根据文档 CSS2DObject,CSS2DObject 是唯一被 CSS2DRenderer 支持的 3D 对象类型。其继承链为:
EventDispatcher → Object3D → CSS2DObject
这意味着 CSS2DObject 拥有 Object3D(src/core/Object3D.js)的全部能力:position、scale、layers、add() 子节点、矩阵更新等,同时又通过继承 EventDispatcher 支持 addEventListener 等事件机制。你可以把它当作一个“纯定位锚点”——它本身不参与 WebGL 绘制,真正的“外观”由外部传入的 DOM 元素决定。
从源码结构看(CSS2DRenderer.js),CSS2DRenderer 被官方描述为 CSS3DRenderer 的简化版:只支持平移(translation)与 2D 旋转(rotation),并明确说明“所有其他类型的可渲染 3D 对象(网格、点云等)都会被忽略”。因此典型用法是双渲染器叠加:WebGLRenderer 负责 3D 内容,CSS2DRenderer 负责 HTML 标签层。
导入方式:作为 Addon 显式引入
CSS2DObject 属于 three.js 的 addon(附加模块),不在核心包中,必须显式导入。CSS2DObject 与 CSS2DRenderer 从同一个模块文件导出(CSS2DRenderer.js 末尾的 export { CSS2DObject, CSS2DRenderer }):
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
在本地开发中,可通过 import map 将 three/addons/ 映射到仓库的 examples/jsm/ 目录,参考 css2d_label.html:
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
构造函数:new CSS2DObject( element )
文档定义的构造函数签名为 new CSS2DObject( element : HTMLElement ),参数是定义该 3D 对象外观的 DOM 元素。结合源码(CSS2DRenderer.js),构造函数还做了几件文档未展开的关键事情:
constructor( element = document.createElement( 'div' ) ) {
super();
this.isCSS2DObject = true; // 类型测试标志
this.element = element;
this.element.style.position = 'absolute'; // 强制绝对定位
this.element.style.userSelect = 'none'; // 禁止文本选中
this.element.setAttribute( 'draggable', false ); // 禁止拖拽
this.center = new Vector2( 0.5, 0.5 ); // 锚点,默认元素中心
this.rotation2D = 0; // 2D 旋转角(弧度,逆时针)
this.addEventListener( 'removed', function () { /* 从场景移除时同步移除 DOM */ } );
}
要点:
- element 可省略:不传参时默认创建一个空
div,方便后续自行填充内容; - 样式会被接管:元素被强制设为
position: absolute且user-select: none,因为渲染器完全依赖transform来定位它;同时draggable=false避免与 OrbitControls 等交互冲突; - 生命周期联动:源码在构造时注册了
removed事件监听(L66-L82),当该对象从场景图中被移除时,会递归traverse子节点并调用object.element.remove()把对应 DOM 从文档中清理掉——这避免了“3D 场景删了对象、页面上却残留标签”的常见内存/DOM 泄漏问题。
附带的 copy() 行为
copy() 方法(L86-L98)在拷贝父类属性的基础上,会用 cloneNode(true) 深拷贝 element,并复制 center 与 rotation2D。也就是说 clone() 得到的 CSS2DObject 是一个全新的独立 DOM 节点,而不是引用原元素,可安全用于实例化/模板场景。
属性详解
.center : Vector2 —— 锚点(pivot)
文档描述:3D 对象的中心点,(0, 0) 到 (1, 1) 之间取值,默认 (0.5, 0.5)。以源码 JSDoc(L48-L56)的语义为准:(0.5, 0.5) 对应元素的中心,(0, 0) 对应元素的左上角。
center 的实际作用在渲染器投影阶段体现(L249-L262):
// pivot point
const cx = 100 * object.center.x;
const cy = 100 * object.center.y;
element.style.transformOrigin = `${cx}% ${cy}%`;
// ...
element.style.transform = `translate(${- cx}%, ${- cy}%) translate(${tx}px, ${ty}px) rotate(${angle}rad)`;
即:渲染器先把 transformOrigin 设为锚点,再通过一段 translate(-cx%, -cy%) translate(tx, ty) 的 CSS transform,使锚点对准投影后的 3D 坐标,元素其余部分相对锚点偏移。这样就能实现“标签左上角贴着物体”“标签右下角贴着物体”等不同的对位方式。
.element : HTMLElement (readonly)
定义该 3D 对象外观的 DOM 元素,只读。注意一个实现细节:CSS2DRenderer 在渲染时如果发现元素的 parentNode 不是渲染器自己的 domElement,会主动执行 domElement.appendChild(element) 把它移入标签容器(L264-L268)。因此你创建的元素不需要(也不应该)手动 append 到页面——一旦进入渲染流程,它的所有权就归 CSS2DRenderer 的容器 div 了。
.isCSS2DObject : boolean (readonly)
类型测试标志,恒为 true,默认值即 true。这是 three.js 一贯的类型判别约定(如 isMesh、isObject3D),渲染器内部正是用它来过滤可渲染对象(L235 的 if ( object.isCSS2DObject ),以及 z 序排序前的 traverseVisible 过滤 L299-L311)。
.rotation2D : number(源码补充)
源码中还提供一个文档未列出的属性 rotation2D(L58-L64):以弧度为单位的逆时针 2D 旋转角,默认 0。渲染时以 rotate(-rotation2D rad) 应用于元素 transform(L255)。配合 position 的 3D 旋转,即可实现“标签随物体 3D 旋转但保持屏幕朝向 + 额外 2D 自转”的效果(与 CSS3DSprite 的 rotation2D 语义一致)。
渲染管线:CSS2DRenderer 如何定位 CSS2DObject
理解 CSS2DRenderer.render( scene, camera ) 的内部流程(L181-L288),是理解 CSS2DObject 属性的关键:
- 更新矩阵:若
scene.matrixWorldAutoUpdate === true则updateMatrixWorld(),游离相机(camera.parent === null)同理; - 构造视图投影矩阵:
_viewProjectionMatrix = camera.projectionMatrix × camera.matrixWorldInverse; - 递归 renderObject:对每个节点
visible === false则递归隐藏(style.display = 'none');- 对
isCSS2DObject节点:取matrixWorld位置 → 用视图投影矩阵变换到裁剪空间; - 可见性判定:
_vector.z >= -1 && _vector.z <= 1(位于裁剪盒内)且object.layers.test( camera.layers ) === true(图层匹配)才显示,否则display: none; - 屏幕坐标换算:
tx = x * width/2 + width/2,ty = -y * height/2 + height/2,再叠加锚点偏移与rotation2D写入transform; - 调用
onBeforeRender/onAfterRender回调; - 记录对象到相机的平方距离(
WeakMap缓存);
- z 序排序(sortObjects):
sortObjects默认为true时,zOrder()(L313-L338)对所有可见CSS2DObject排序——先按renderOrder降序,再按到相机的距离升序——然后依次写入z-index,保证近处标签(或更高renderOrder的标签)叠在远处标签之上。若设为false则不分配 z-index。
两个重要的使用约束(来自 CSS2DRenderer 文档与源码注释):
- 该渲染器只支持 100% 的浏览器缩放与显示缩放,非 100% 缩放下标签位置会失准;
CSS2DRenderer会忽略场景中的网格、点云等其他对象,它只服务于CSS2DObject子树。
实战示例:行星标签(css2d_label)
仓库中的 examples/css2d_label.html 是官方示例,展示了完整的“WebGL + CSS2D 双渲染器”架构。核心代码:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
// 1. 创建 DOM 标签并包装成 CSS2DObject
const earthDiv = document.createElement( 'div' );
earthDiv.className = 'label';
earthDiv.textContent = 'Earth';
const earthLabel = new CSS2DObject( earthDiv );
earthLabel.position.set( 1.5 * EARTH_RADIUS, 0, 0 ); // 放在地球右侧 1.5 倍半径处
earthLabel.center.set( 0, 1 ); // 锚点设为元素左下/左上区域,标签向外展开
earth.add( earthLabel ); // 挂到地球 mesh 下,随其一起变换
earthLabel.layers.set( 0 ); // 放入 layer 0
// 2. 两个渲染器分别负责 WebGL 内容与 HTML 标签层
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
document.body.appendChild( labelRenderer.domElement );
// 3. OrbitControls 绑定到标签层容器(标签层在最上方,保证交互可用)
const controls = new OrbitControls( camera, labelRenderer.domElement );
// 4. 渲染循环中双渲染
renderer.render( scene, camera );
labelRenderer.render( scene, camera );
几个值得注意的实战细节:
center.set( 0, 1 )与center.set( 0, 0 )的组合:示例中给地球同时挂了“名称”与“质量”两个标签,用不同锚点让它们一个从左侧向右展开、一个向下排布,避免重叠(L130-L172);- layers 过滤:标签通过
layers.set( 0 / 1 )分组,相机切换camera.layers.toggle(...)即可整体显示/隐藏某组标签——这正是渲染管线中object.layers.test( camera.layers )可见性判定的直接应用; - resize 时两个渲染器都要
setSize(L199-L209),否则投影坐标与屏幕尺寸不一致。
另一个典型用法:分子原子标签(webgl_loader_pdb)
examples/webgl_loader_pdb.html 展示了 CSS2DObject 在数据可视化中的用法:加载 PDB 分子结构后,为每个原子生成带颜色的 HTML 文本标签(L188-L195):
const text = document.createElement( 'div' );
text.className = 'label';
text.style.color = 'rgb(' + atom[ 3 ][ 0 ] + ',' + atom[ 3 ][ 1 ] + ',' + atom[ 3 ][ 2 ] + ')';
text.textContent = atom[ 4 ]; // 原子符号,如 C、O、N
const label = new CSS2DObject( text );
label.position.copy( object.position ); // 与原子球体同坐标
root.add( label );
这里的标签挂在根节点下并与网格共享坐标,直观说明 CSS2DObject 的 position 与普通 3D 对象完全同义——它只是把“渲染产物”从 GPU 三角形换成了 DOM 节点。仓库中 examples/ 目录下还有 css2d_mixed.html、css2d_sandbox.html、css2d_molecules.html 等更多变体可作参考。
使用注意事项小结
| 事项 | 说明 | 依据 |
|---|---|---|
| 必须显式导入 | Addon 模块,CSS2DObject 与 CSS2DRenderer 同文件导出 |
CSS2DRenderer.js |
| 元素所有权 | 进入渲染流程后元素会被移入渲染器 domElement,勿自行 append |
L264-L268 |
| 锚点设置 | center 为 Vector2,(0.5, 0.5) 居中,(0, 0) 对应左上角 |
L48-L56 |
| 层级遮挡 | 由 sortObjects(默认 true)按 renderOrder + 相机距离分配 z-index |
L313-L338 |
| 显隐控制 | visible 与相机 layers 双重控制;裁剪盒外(z 超出 [-1, 1])自动隐藏 |
L237-L243 |
| 缩放限制 | 仅支持 100% 浏览器/显示缩放 | CSS2DRenderer 文档 |
| 清理 | 从场景移除对象时 DOM 会被自动 remove() |
L66-L82 |
源码与文档索引
- 类实现:examples/jsm/renderers/CSS2DRenderer.js(
CSS2DObject位于 L14-L100,CSS2DRenderer位于 L122-L342) - API 文档:CSS2DObject、CSS2DRenderer、CSS3DSprite
- 官方示例:css2d_label.html、webgl_loader_pdb.html、css2d_sandbox.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
