首页
/ Three.js CSS2DObject 详解:把 DOM 元素包装成 3D 对象的实现与实战

Three.js CSS2DObject 详解:把 DOM 元素包装成 3D 对象的实现与实战

2026-09-06 13:02:47作者:庞眉杨Will

CSS2DObject 是 three.js 中专门配合 CSS2DRenderer 使用的 3D 对象类型:它把一个普通 DOM 元素(HTMLElement)包装进场景图,让 HTML 标签、卡片、图标可以跟随 3D 场景中的坐标进行透视投影定位。本文基于官方文档 CSS2DObject 与 addon 源码 CSS2DRenderer.js,完整讲解其继承关系、构造参数、center / element / isCSS2DObject 属性,以及它在渲染管线中如何完成投影、可见性判断、z 序排序,并给出可运行的完整示例代码。

three.js css2d label 示例运行效果:地球与月球模型旁跟随显示 HTML 文字标签

定位与继承体系:场景图中唯一的“HTML 节点”

根据文档 CSS2DObjectCSS2DObject唯一CSS2DRenderer 支持的 3D 对象类型。其继承链为:

EventDispatcher → Object3D → CSS2DObject

这意味着 CSS2DObject 拥有 Object3Dsrc/core/Object3D.js)的全部能力:positionscalelayersadd() 子节点、矩阵更新等,同时又通过继承 EventDispatcher 支持 addEventListener 等事件机制。你可以把它当作一个“纯定位锚点”——它本身不参与 WebGL 绘制,真正的“外观”由外部传入的 DOM 元素决定。

从源码结构看(CSS2DRenderer.js),CSS2DRenderer 被官方描述为 CSS3DRenderer 的简化版:只支持平移(translation)与 2D 旋转(rotation),并明确说明“所有其他类型的可渲染 3D 对象(网格、点云等)都会被忽略”。因此典型用法是双渲染器叠加WebGLRenderer 负责 3D 内容,CSS2DRenderer 负责 HTML 标签层。

导入方式:作为 Addon 显式引入

CSS2DObject 属于 three.js 的 addon(附加模块),不在核心包中,必须显式导入。CSS2DObjectCSS2DRenderer 从同一个模块文件导出(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: absoluteuser-select: none,因为渲染器完全依赖 transform 来定位它;同时 draggable=false 避免与 OrbitControls 等交互冲突;
  • 生命周期联动:源码在构造时注册了 removed 事件监听(L66-L82),当该对象从场景图中被移除时,会递归 traverse 子节点并调用 object.element.remove() 把对应 DOM 从文档中清理掉——这避免了“3D 场景删了对象、页面上却残留标签”的常见内存/DOM 泄漏问题。

附带的 copy() 行为

copy() 方法(L86-L98)在拷贝父类属性的基础上,会用 cloneNode(true) 深拷贝 element,并复制 centerrotation2D。也就是说 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 一贯的类型判别约定(如 isMeshisObject3D),渲染器内部正是用它来过滤可渲染对象(L235if ( object.isCSS2DObject ),以及 z 序排序前的 traverseVisible 过滤 L299-L311)。

.rotation2D : number(源码补充)

源码中还提供一个文档未列出的属性 rotation2DL58-L64):以弧度为单位的逆时针 2D 旋转角,默认 0。渲染时以 rotate(-rotation2D rad) 应用于元素 transform(L255)。配合 position 的 3D 旋转,即可实现“标签随物体 3D 旋转但保持屏幕朝向 + 额外 2D 自转”的效果(与 CSS3DSpriterotation2D 语义一致)。

渲染管线:CSS2DRenderer 如何定位 CSS2DObject

理解 CSS2DRenderer.render( scene, camera ) 的内部流程(L181-L288),是理解 CSS2DObject 属性的关键:

  1. 更新矩阵:若 scene.matrixWorldAutoUpdate === trueupdateMatrixWorld(),游离相机(camera.parent === null)同理;
  2. 构造视图投影矩阵_viewProjectionMatrix = camera.projectionMatrix × camera.matrixWorldInverse
  3. 递归 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/2ty = -y * height/2 + height/2,再叠加锚点偏移与 rotation2D 写入 transform
    • 调用 onBeforeRender / onAfterRender 回调;
    • 记录对象到相机的平方距离(WeakMap 缓存);
  4. 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 时两个渲染器都要 setSizeL199-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 );

这里的标签挂在根节点下并与网格共享坐标,直观说明 CSS2DObjectposition 与普通 3D 对象完全同义——它只是把“渲染产物”从 GPU 三角形换成了 DOM 节点。仓库中 examples/ 目录下还有 css2d_mixed.htmlcss2d_sandbox.htmlcss2d_molecules.html 等更多变体可作参考。

使用注意事项小结

事项 说明 依据
必须显式导入 Addon 模块,CSS2DObjectCSS2DRenderer 同文件导出 CSS2DRenderer.js
元素所有权 进入渲染流程后元素会被移入渲染器 domElement,勿自行 append L264-L268
锚点设置 centerVector2(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

源码与文档索引

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