three.js CSS3DSprite 深度解析:让 DOM 元素以精灵形式参与 3D 场景编排
本文围绕 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,而 CSS3DObject 又 extends 核心库的 Object3D(文件头部从 'three' 导入 Matrix4、Object3D、Quaternion、Vector3)。这带来三层含义:
- 它是完整的场景图节点:继承自
Object3D,因此天然拥有position、rotation、scale、matrixWorld、layers、visible等全部能力,可以scene.add()进场景,也可以嵌套子节点; - 它携带一个 DOM 元素:继承自
CSS3DObject,构造时传入的element被自动设置position: absolute、pointerEvents: auto、userSelect: none并禁用拖拽,成为这个 3D 节点的“外观”(见 CSS3DRenderer.js); - 它覆写了朝向行为:普通
CSS3DObject会完整使用自身的matrixWorld(含旋转),而CSS3DSprite在渲染时丢弃自身朝向,替换为始终面向相机的 billboard 矩阵,这是“精灵”语义的核心。
二、引入方式:它是 Addon,必须显式导入
与 Mesh、PointLight 等核心类不同,CSS3DSprite 属于 addons(扩展库),不在 three 主包入口里,需要显式从 three/addons 路径导入(参见官方文档 CSS3DSprite.html.md 的 Import 一节):
import { CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';
注意 CSS3DSprite 与 CSS3DObject、CSS3DRenderer 三者定义在同一个模块文件 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参数可选:缺省时自动创建一个空div(element = document.createElement('div')),你完全可以在new之后再向object.element内部填充任意 HTML 内容(文字、图标、表单等);- 生命周期自动管理:父类注册了
removed监听器,当CSS3DSprite(含子节点)从场景图中被移除时,会traverse遍历并把仍挂在 DOM 树上的元素逐个element.remove(),避免 DOM 泄漏。
3.2 属性一览
isCSS3DSprite : boolean(readonly)
类型测试标志,默认 true(CSS3DSprite.html.md Properties 一节)。源码中赋值于 L111。渲染器正是用它来区分精灵与普通对象——renderObject 内部以 if ( object.isCSS3DSprite ) 分支决定是否走 billboard 逻辑(L385)。官方示例 css3d_molecules.html 里则同时使用了 instanceof CSS3DSprite 判断,两种写法在该类上都成立。
rotation2D : number
精灵的 2D 旋转角度,单位为弧度,默认 0(L119)。这是 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 用完整世界矩阵
}
各步骤含义如下:
- 取相机的“朝向基”:
camera.matrixWorldInverse是相机世界矩阵的逆,转置后其前三列即相机在世界空间中三条轴的方向(行向量形式)。用它替换掉对象的旋转部分,对象就“复制”了相机的朝向——这正是经典 billboard 矩阵的构造方法(源码注释引出了 swiftcoder 2008 年的同名文章)。 - 叠加
rotation2D:若rotation2D !== 0,右乘一个makeRotationZ( rotation2D )。注意这是在 billboard 平面内部旋转,效果等同于把贴面图案转个角度,而不会改变“始终面向相机”的性质;判断用!== 0而非!= 0,0值时跳过矩阵乘法省一次运算。 - 保留对象自己的位置与缩放:从
object.matrixWorld中decompose出_position与_scale,再写回_matrix。也就是说CSS3DSprite的position和scale依然完全受你控制,只有旋转自由度被相机朝向接管——这也解释了为什么官方示例中通过改object.scale就能让精灵做“呼吸”式缩放动画。 - 清零第四列:
elements[3/7/11] = 0、elements[15] = 1把平移分量清零,是因为位置已经通过setPosition写入,需要消除相机矩阵自带的平移项,保证对象停留在场景中的真实坐标上。 - 非精灵对象则直接使用
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 = 750,phi = 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 同时导入了 CSS3DObject 与 CSS3DSprite(L44),并用 instanceof CSS3DSprite 对两类对象分别做差异化动画,说明二者并存是受支持的组合方式。
七、限制与注意事项
CSS3DRenderer 的源码头部注释(L140-L153)明确列出三条重要限制,使用 CSS3DSprite 时同样适用:
- 不能使用 three.js 的材质系统(no material system)——外观完全由 CSS 决定,
material、光照、贴图管线均不生效; - 不能与几何体(Geometry)结合——它不是网格,没有 UV、法线、顶点;
- 仅支持 100% 的浏览器与显示器缩放——页面缩放/高 DPI 缩放会破坏像素级变换对齐。
此外由源码结构还可以确认两点使用前提:
- 渲染器依赖浏览器的 CSS 3D transforms 能力(
preserve-3d、matrix3d),且所有精灵元素共享同一个cameraElement变换栈,因此与 WebGL 场景叠加时需注意两层 DOM 的层级与指针事件(pointerEvents在viewElement上被置为none,元素自身为auto); - 由于样式写入带字符串级缓存,动画中若只改
rotation2D之外的属性而没有触发matrixWorld更新,务必保持scene.matrixWorldAutoUpdate为默认值或手动updateMatrixWorld(),否则精灵位置不会刷新。
八、延伸阅读(仓库内路径)
- API 文档页:docs/pages/CSS3DSprite.html.md、docs/pages/CSS3DObject.html.md、docs/pages/CSS3DRenderer.html.md
- 核心实现:examples/jsm/renderers/CSS3DRenderer.js(
CSS3DSprite类定义于 L93-L133,billboard 分支于 L385-L403) - 官方示例:css3d_sprites(512 精灵粒子系统)、css3d_molecules(Object 与 Sprite 混用)、css3d_sandbox
- 示例注册表:examples/files.json 中
css3d分组列出了全部 7 个 css3d 示例
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