首页
/ three.js CSS3DSprite 深度解析:让 DOM 元素以精灵形式参与 3D 场景编排

three.js CSS3DSprite 深度解析:让 DOM 元素以精灵形式参与 3D 场景编排

2026-09-06 13:17:42作者:丁柯新Fawn

本文围绕 three.js 官方 API 文档中的 CSS3DSprite 展开,系统讲解这个将 HTML 元素转换为 3D 精灵(billboard)的内置对象:从它的继承链、构造参数、rotation2D 等属性,到 CSS3DRenderer 源码 中 billboard 矩阵的逐段实现原理,并完整还原官方示例 css3d_sprites 的 512 个精灵粒子系统的搭建过程。读完后你将能够独立使用 CSS3DSprite 把 DOM 元素以始终面向相机的形态接入 three.js 场景图,并理解渲染器如何把 Object3D 的变换矩阵翻译成 CSS matrix3d() 变换。

一、CSS3DSprite 的定位与继承链

官方文档 CSS3DSprite.html.md 对其定义非常明确:

A specialized version of CSS3DObject that represents DOM elements as sprites. (CSS3DObject 的特化版本,将 DOM 元素表示为精灵。)

其继承链为:

EventDispatcher → Object3D → CSS3DObject → CSS3DSprite

这一继承结构可以从源码中得到印证。在 CSS3DRenderer.js 中,CSS3DSprite 直接 extends 同文件内定义的 CSS3DObject,而 CSS3DObjectextends 核心库的 Object3D(文件头部从 'three' 导入 Matrix4Object3DQuaternionVector3)。这带来三层含义:

  1. 它是完整的场景图节点:继承自 Object3D,因此天然拥有 positionrotationscalematrixWorldlayersvisible 等全部能力,可以 scene.add() 进场景,也可以嵌套子节点;
  2. 它携带一个 DOM 元素:继承自 CSS3DObject,构造时传入的 element 被自动设置 position: absolutepointerEvents: autouserSelect: none 并禁用拖拽,成为这个 3D 节点的“外观”(见 CSS3DRenderer.js);
  3. 它覆写了朝向行为:普通 CSS3DObject 会完整使用自身的 matrixWorld(含旋转),而 CSS3DSprite 在渲染时丢弃自身朝向,替换为始终面向相机的 billboard 矩阵,这是“精灵”语义的核心。

二、引入方式:它是 Addon,必须显式导入

MeshPointLight 等核心类不同,CSS3DSprite 属于 addons(扩展库),不在 three 主包入口里,需要显式从 three/addons 路径导入(参见官方文档 CSS3DSprite.html.md 的 Import 一节):

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

注意 CSS3DSpriteCSS3DObjectCSS3DRenderer 三者定义在同一个模块文件 examples/jsm/renderers/CSS3DRenderer.js 中,文件末尾统一导出:

export { CSS3DObject, CSS3DSprite, CSS3DRenderer };

因此在项目中实际写导入时,通常会一次性引入所需的多个类:

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

官方示例 css3d_sprites.html 正是这样使用的,并配合 importmap 将 three/addons/ 映射到 ./jsm/ 目录。

三、构造函数与实例属性

3.1 new CSS3DSprite( element : HTMLElement )

构造函数签名与文档一致(CSS3DSprite.html.md 的 Constructor 一节):

  • element:要包装的 DOM 元素,类型为 HTMLElement

从源码(CSS3DRenderer.js)看,CSS3DSprite 的构造函数本身非常轻:

constructor( element ) {

    super( element );                 // 交给 CSS3DObject 处理 element
    this.isCSS3DSprite = true;
    this.rotation2D = 0;
}

真正的 element 处理逻辑在父类 CSS3DObject 中:

constructor( element = document.createElement( 'div' ) ) {   // element 缺省为新建 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 );
    // 监听 removed 事件,对象移出场景时自动清理 DOM
    this.addEventListener( 'removed', function () { /* traverse 并 element.remove() */ } );
}

这里有两个实用细节值得注意:

  • element 参数可选:缺省时自动创建一个空 divelement = document.createElement('div')),你完全可以在 new 之后再向 object.element 内部填充任意 HTML 内容(文字、图标、表单等);
  • 生命周期自动管理:父类注册了 removed 监听器,当 CSS3DSprite(含子节点)从场景图中被移除时,会 traverse 遍历并把仍挂在 DOM 树上的元素逐个 element.remove(),避免 DOM 泄漏。

3.2 属性一览

isCSS3DSprite : boolean(readonly)

类型测试标志,默认 trueCSS3DSprite.html.md Properties 一节)。源码中赋值于 L111。渲染器正是用它来区分精灵与普通对象——renderObject 内部以 if ( object.isCSS3DSprite ) 分支决定是否走 billboard 逻辑(L385)。官方示例 css3d_molecules.html 里则同时使用了 instanceof CSS3DSprite 判断,两种写法在该类上都成立。

rotation2D : number

精灵的 2D 旋转角度,单位为弧度,默认 0L119)。这是 CSS3DSprite 相对于 CSS3DObject 唯一新增的状态量:它不参与 3D 空间中的四元数朝向,而是在 billboard 平面内对元素做绕 Z 轴的旋转(实现细节见下节)。

继承自 CSS3DObject 的属性

  • element : HTMLElement(readonly):定义该 3D 对象外观的 DOM 元素,默认为新建的 div。这一点在文档 CSS3DObject.html.md 中有说明。
  • isCSS3DObject : boolean(readonly):默认为 true,用于类型测试。

copy() 的行为补充

CSS3DSprite 覆写了 copy()CSS3DRenderer.js):在调用 super.copy()(父类会执行 element.cloneNode( true ) 深拷贝 DOM 节点)之后,额外拷贝 rotation2D。因此用 spriteB.copy( spriteA ) 复制精灵时,元素外观与 2D 旋转角都会被保留——这是从源码结构看可以直接确认的实现事实。

四、核心原理:渲染器如何生成 billboard 矩阵

CSS3DSprite 的“精灵”魔法发生在 CSS3DRenderer.renderObject() 的渲染阶段。下面逐段解析 CSS3DRenderer.js 中的关键代码:

if ( object.isCSS3DSprite ) {

    // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/

    _matrix.copy( camera.matrixWorldInverse );
    _matrix.transpose();

    if ( object.rotation2D !== 0 ) _matrix.multiply( _matrix2.makeRotationZ( object.rotation2D ) );

    object.matrixWorld.decompose( _position, _quaternion, _scale );
    _matrix.setPosition( _position );
    _matrix.scale( _scale );

    _matrix.elements[ 3 ] = 0;
    _matrix.elements[ 7 ] = 0;
    _matrix.elements[ 11 ] = 0;
    _matrix.elements[ 15 ] = 1;

    style = getObjectCSSMatrix( _matrix );

} else {

    style = getObjectCSSMatrix( object.matrixWorld );   // 普通 CSS3DObject 用完整世界矩阵
}

各步骤含义如下:

  1. 取相机的“朝向基”camera.matrixWorldInverse 是相机世界矩阵的逆,转置后其前三列即相机在世界空间中三条轴的方向(行向量形式)。用它替换掉对象的旋转部分,对象就“复制”了相机的朝向——这正是经典 billboard 矩阵的构造方法(源码注释引出了 swiftcoder 2008 年的同名文章)。
  2. 叠加 rotation2D:若 rotation2D !== 0,右乘一个 makeRotationZ( rotation2D )。注意这是在 billboard 平面内部旋转,效果等同于把贴面图案转个角度,而不会改变“始终面向相机”的性质;判断用 !== 0 而非 != 00 值时跳过矩阵乘法省一次运算。
  3. 保留对象自己的位置与缩放:从 object.matrixWorlddecompose_position_scale,再写回 _matrix。也就是说 CSS3DSpritepositionscale 依然完全受你控制,只有旋转自由度被相机朝向接管——这也解释了为什么官方示例中通过改 object.scale 就能让精灵做“呼吸”式缩放动画。
  4. 清零第四列elements[3/7/11] = 0elements[15] = 1 把平移分量清零,是因为位置已经通过 setPosition 写入,需要消除相机矩阵自带的平移项,保证对象停留在场景中的真实坐标上。
  5. 非精灵对象则直接使用 getObjectCSSMatrix( object.matrixWorld ),即完整世界矩阵(含自身四元数旋转),这是它与 CSS3DSprite 在渲染上的唯一分叉点。

生成的矩阵最终由 getObjectCSSMatrix() 序列化为 CSS 变换字符串,注意它会在 matrix3d(...) 之前前置 translate(-50%,-50%),把元素自身的中心对齐到 3D 坐标原点——这样你在 CSS 中写的元素宽高不会再让 3D 定位偏移半个元素。

渲染管线中的其他关键机制

围绕这段 billboard 逻辑,renderObject()L362-L440)还包含几个值得了解的机制:

  • 可见性双重门控object.visible === false 会递归 hideObject;此外还执行 object.layers.test( camera.layers ),即 CSS3D 对象同样受 layers 系统控制,与 WebGL 渲染器的分层机制一致;
  • 样式缓存:每个对象有一层 WeakMap 缓存(cache.objects),只有新算出的 style 字符串与缓存不同才会真正写 element.style.transform,相机/场景静止时不产生任何 DOM 写入;
  • 钩子函数:每个可见对象在更新变换前后会回调 object.onBeforeRender( renderer, scene, camera ) / object.onAfterRender( ... ),你可以在这里动态修改 element 的 class 或 rotation2D
  • DOM 挂载:元素统一挂载到渲染器内部的 cameraElement 容器下(不在时执行 appendChild),该容器带 transformStyle: preserve-3d,构成整条 CSS 3D 变换链的根。

另外在 render() 主流程(L218-L267)中,渲染器会把相机的投影信息翻译成 CSS:透视相机用 perspective(fov) + translateZ(fov) + matrix3d(相机逆矩阵),正交相机则用 scale + translate 组合;fov 取自 camera.projectionMatrix.elements[5] * heightHalf。理解这一段后就能明白:CSS3DSprite 的位置/缩放是“真 3D”的(随相机距离自动变大变小),而它的朝向永远平行于屏幕。

五、实战:完整复刻官方 css3d_sprites 示例

官方示例 examples/css3d_sprites.html 演示了 512 个 CSS3DSprite 在“波浪平面 / 立方体 / 随机散布 / 球面”四种布局间 TWEEN 过渡,是理解本类最佳用法的范本。核心步骤如下(当前仓库为 three.js r185 版本):

1. 场景与相机

camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 5000 );
camera.position.set( 600, 400, 1500 );
camera.lookAt( 0, 0, 0 );

scene = new THREE.Scene();

2. 用同一张纹理图克隆出 512 个精灵

const image = document.createElement( 'img' );
image.addEventListener( 'load', function () {

    for ( let i = 0; i < particlesTotal; i ++ ) {       // particlesTotal = 512

        const object = new CSS3DSprite( image.cloneNode() );  // 每个精灵独立持有 img 克隆
        object.position.x = Math.random() * 4000 - 2000;
        object.position.y = Math.random() * 4000 - 2000;
        object.position.z = Math.random() * 4000 - 2000;
        scene.add( object );
        objects.push( object );
    }

    transition();
} );
image.src = 'textures/sprite.png';

注意这里对 <img> 调用 cloneNode()(不带深度)——因为 CSS3DSprite 构造后渲染器只认 object.element 本身,五个以上精灵若共享同一元素引用就会互相抢占挂载点,克隆是必要操作。

3. 预生成四种目标布局的位置数组

示例中按 512 个粒子依次填充四段位置:正弦起伏的平面(amountX = 16, amountZ = 32, separationPlane = 150)、8×8×8 立方体网格、随机立方区域、以及斐波那契球面分布(radius = 750phi = acos(-1 + 2i/N))。这些只是示例数据,实际项目中替换成你的布局算法即可。

4. 创建 CSS3DRenderer 并接入容器

renderer = new CSS3DRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.getElementById( 'container' ).appendChild( renderer.domElement );

CSS3DRenderer 构造函数可选传入 { element } 指定容器(缺省自建 div 并设 overflow: hidden);domElement 内部嵌套 viewElement → cameraElement 两层 div,所有精灵元素最终被挂到 cameraElement 下(源码 L187-L196)。

5. 交互控制与每帧动画

controls = new TrackballControls( camera, renderer.domElement );   // 用 CSS3D 的 domElement 而非 WebGL canvas

function animate() {

    requestAnimationFrame( animate );

    TWEEN.update();
    controls.update();

    const time = performance.now();

    for ( let i = 0, l = objects.length; i < l; i ++ ) {
        const object = objects[ i ];
        const scale = Math.sin( ( Math.floor( object.position.x ) + time ) * 0.002 ) * 0.3 + 1;
        object.scale.set( scale, scale, scale );   // 缩放受控,朝向永远面向相机
    }

    renderer.render( scene, camera );
}

布局切换由 transition() 完成:对每个精灵的 position 建立 TWEEN.Tween,随机化时长(2000~4000 ms)并施加 Exponential.InOut 缓动,同时用一条空 tween 定时触发下一次 transition(),实现四种布局的无限轮播。

6. 窗口缩放适配

window.addEventListener( 'resize', function () {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
} );

该示例的仓库截图见 css3d_sprites.jpg

六、CSS3DSprite 与 CSS3DObject 的选型对比

维度 CSS3DObject CSS3DSprite
朝向 使用对象自身四元数旋转(matrixWorld 原样下发) 朝向被替换为相机朝向(billboard),自身 rotation 不生效
独有属性 rotation2D(平面内 2D 旋转,弧度,默认 0)
典型场景 3D 网页沙盒、可翻转卡片、分子模型的固定朝向面 粒子系统、地图标注、HUD 标签、始终朝向镜头的徽标
位置/缩放 完全受 position/scale 控制 同样受 position/scale 控制
类型判断 isCSS3DObject isCSS3DSprite(渲染器内部分支即依据它)

两者可以混用在同一场景中——另一个官方示例 css3d_molecules.html 同时导入了 CSS3DObjectCSS3DSpriteL44),并用 instanceof CSS3DSprite 对两类对象分别做差异化动画,说明二者并存是受支持的组合方式。

七、限制与注意事项

CSS3DRenderer 的源码头部注释(L140-L153)明确列出三条重要限制,使用 CSS3DSprite 时同样适用:

  • 不能使用 three.js 的材质系统(no material system)——外观完全由 CSS 决定,material、光照、贴图管线均不生效;
  • 不能与几何体(Geometry)结合——它不是网格,没有 UV、法线、顶点;
  • 仅支持 100% 的浏览器与显示器缩放——页面缩放/高 DPI 缩放会破坏像素级变换对齐。

此外由源码结构还可以确认两点使用前提:

  • 渲染器依赖浏览器的 CSS 3D transforms 能力(preserve-3dmatrix3d),且所有精灵元素共享同一个 cameraElement 变换栈,因此与 WebGL 场景叠加时需注意两层 DOM 的层级与指针事件(pointerEventsviewElement 上被置为 none,元素自身为 auto);
  • 由于样式写入带字符串级缓存,动画中若只改 rotation2D 之外的属性而没有触发 matrixWorld 更新,务必保持 scene.matrixWorldAutoUpdate 为默认值或手动 updateMatrixWorld(),否则精灵位置不会刷新。

八、延伸阅读(仓库内路径)

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